Деплой смарт-контрактов в Near Protocol
Разработчики, приходящие из EVM-экосистемы, обычно ждут чего-то похожего на Solidity + Hardhat. Near работает иначе: контракты — это WebAssembly-модули, аккаунт-модель принципиально отличается от Ethereum, а хранилище данных платит сам разработчик, а не пользователь. Эти отличия нужно понять до написания первой строки кода, иначе деплой превратится в серию неприятных сюрпризов.
Среда выполнения и языки
Near VM исполняет WASM-байткод. Официально поддерживаются два языка:
Rust + near-sdk-rs — основной выбор для production. SDK предоставляет макросы #[near_bindgen], #[init], BorshSerialize/BorshDeserialize для сериализации состояния. Borsh (Binary Object Representation Serializer for Hashing) — детерминированный бинарный формат, обязательный для хранения состояния контрактов Near.
JavaScript/TypeScript + near-sdk-js — для прототипов и несложной логики. Компилируется в WASM через QuickJS, производительность заметно ниже, лимиты по gas жёстче.
Пример минимального Rust-контракта:
use near_sdk::{near_bindgen, env, AccountId};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Counter {
value: i64,
}
#[near_bindgen]
impl Counter {
#[init]
pub fn new() -> Self {
Self { value: 0 }
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn get(&self) -> i64 {
self.value
}
}
Аккаунт-модель и хранилище
Ключевое отличие от EVM: в Near каждый смарт-контракт живёт на именованном аккаунте (myapp.near, myapp.testnet). Аккаунт хранит код контракта и его состояние. Стоимость хранения — 1 NEAR за 100 КБ состояния, и эти средства блокируются на балансе аккаунта. Это называется storage staking.
Практическое следствие: если контракт хранит данные пользователей, нужна логика storage_deposit — пользователь вносит NEAR-депозит перед регистрацией, эти средства покрывают его storage. Стандарт NEP-145 определяет этот паттерн для fungible tokens.
#[payable]
pub fn storage_deposit(&mut self, account_id: Option<AccountId>) -> StorageBalance {
let amount = env::attached_deposit();
let account_id = account_id.unwrap_or_else(env::predecessor_account_id);
// минимум 0.00125 NEAR за запись аккаунта
assert!(amount >= STORAGE_PER_ACCOUNT, "Insufficient deposit");
// ...
}
Sub-аккаунты — стандартный паттерн для factory-контрактов: token.myapp.near, pool.myapp.near. Каждый sub-аккаунт — полноценный независимый аккаунт.
Кросс-контрактные вызовы и async-модель
Near — асинхронный шардированный блокчейн. Кросс-контрактный вызов не выполняется синхронно внутри транзакции — он создаёт отдельный receipt, который обрабатывается в следующем блоке. Результат приходит в callback.
#[near_bindgen]
impl MyContract {
pub fn call_external(&mut self, contract_id: AccountId) -> Promise {
ext_other_contract::ext(contract_id)
.with_static_gas(Gas(5 * TGAS))
.get_value()
.then(
Self::ext(env::current_account_id())
.with_static_gas(Gas(5 * TGAS))
.on_value_received()
)
}
#[private]
pub fn on_value_received(&mut self, #[callback_result] result: Result<u64, PromiseError>) {
match result {
Ok(value) => { /* обработка */ }
Err(_) => { env::panic_str("External call failed") }
}
}
}
#[private] — макрос, добавляющий проверку assert_eq!(env::current_account_id(), env::predecessor_account_id()). Без него любой может вызвать callback напрямую.
Сборка и деплой
Инструментарий: cargo-near — официальный CLI для сборки, near-cli-rs (Rust-переписанная версия) для деплоя.
# Установка
cargo install cargo-near
cargo install near-cli-rs
# Сборка оптимизированного WASM
cargo near build
# Деплой на testnet
near contract deploy myapp.testnet \
use-file ./target/near/myapp.wasm \
without-init-call \
network-config testnet \
sign-with-keychain \
send
WASM-файл после сборки с cargo-near проходит через wasm-opt (Binaryen) автоматически — это уменьшает размер и экономит на storage.
Для mainnet деплой через multisig-аккаунт или через near-cli с hardware wallet (--useLedgerKey). Деплой без инициализации — контракт в неинициализированном состоянии, первый вызов new() задаёт начальное состояние.
Тестирование
Unit-тесты — стандартные Rust unit tests с VMContextBuilder для мока окружения (predecessor, attached_deposit, block_timestamp).
Workspaces-rs — интеграционные тесты, запускают локальный sandbox Near и деплоят реальный WASM. Это де-факто стандарт для тестирования кросс-контрактных взаимодействий:
#[tokio::test]
async fn test_full_flow() -> anyhow::Result<()> {
let worker = near_workspaces::sandbox().await?;
let wasm = std::fs::read("./target/near/myapp.wasm")?;
let contract = worker.dev_deploy(&wasm).await?;
let result = contract.call("increment")
.transact().await?;
assert!(result.is_success());
Ok(())
}
Типичные ошибки при переносе с EVM
-
Нет
msg.senderкак адреса. В Nearenv::predecessor_account_id()— строка, не 20 байт. Сравнение через==дляAccountId. -
Паника вместо revert.
env::panic_str("message")илиassert!()— аналогrequire()в Solidity. Стоимость газа за панику не возвращается. -
Итерация по
LookupMap.LookupMapне итерируемый. Для итерируемых коллекций —UnorderedMap,Vector. Выбор структуры данных влияет на gas. - Gas единицы. 1 TGas = 10^12 gas. Простой вызов ~2-3 TGas, кросс-контрактный +5 TGas минимум. Лимит транзакции — 300 TGas.
Процесс работы
Аудит требований → выбор языка (Rust/JS) → проектирование storage-модели (критично для cost) → разработка с unit-тестами → интеграционное тестирование через workspaces → деплой на testnet → верификация через Near Explorer → деплой на mainnet.
Срок 4 часа — это деплой готового кода. Разработка контракта с нуля с тестированием: 1-5 дней в зависимости от сложности логики.







