Розгортування смарт-контрактів у 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, продуктивність значно нижча, ліміти газу суворіші.
Приклад мінімального 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 перед реєстрацією, ці засоби покривають його зберігання. Стандарт 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");
// ...
}
Підоблікові записи — стандартний паттерн для factory контрактів: token.myapp.near, pool.myapp.near. Кожен підоблік — повністю незалежний обліковий запис.
Кросконтрактні виклики та асинхронна модель
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) — це зменшує розмір та економить на зберіганні.
Для розгортування mainnet через multisig обліковий запис або через near-cli з апаратним гаманцем (--useLedgerKey). Розгортування без ініціалізації — контракт у ненієнціалізованому стані, перший виклик new() встановлює початковий стан.
Тестування
Unit тести — стандартні Rust unit тести з VMContextBuilder для моку середовища (predecessor, attached_deposit, block_timestamp).
Workspaces-rs — інтеграційні тести, які запускають локальну Near sandbox та розгортують реальний 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. Вибір структури даних впливає на газ. - Одиниці газу. 1 TGas = 10^12 газу. Простий виклик ~2-3 TGas, кросконтрактний +5 TGas мінімум. Ліміт транзакції — 300 TGas.
Процес роботи
Аудит вимог → вибір мови (Rust/JS) → проектування моделі зберігання (критично для вартості) → розробка з unit тестами → інтеграційне тестування через workspaces → розгортування на testnet → верифікація через Near Explorer → розгортування на mainnet.
Терміни 4 години — це розгортування готового коду. Розробка контракту з нуля з тестуванням: 1-5 днів залежно від складності логіки.







