Deploying Smart Contracts on Near: Rust, Accounts, and Storage
When a DeFi startup team decided to migrate its liquidity protocol to Near, the first contract consumed 15 NEAR on storage due to incorrect data structures — 15,000 user balance entries were stored in a Vector when they needed a LookupMap. That's when we faced the core difference between Near and EVM: here, the developer pays for storage. Our 5+ years of experience in the Near ecosystem helps avoid such mistakes and save up to 40% on storage costs with proper design. Let’s break down how to do it right so you don’t waste deposits.
How Near’s Account Model Works
In Near, each smart contract lives on a named account (myapp.near, myapp.testnet). The account stores the contract code and its state. The storage cost is 1 NEAR per 100 KB of state, and these funds are locked on the account balance. This is called storage staking.
Practical consequence: if the contract stores user data, you need storage_deposit logic — the user deposits NEAR before registration, and those funds cover their storage. The NEP-145 standard defines this pattern for 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);
// minimum 0.00125 NEAR per account record
assert!(amount >= STORAGE_PER_ACCOUNT, "Insufficient deposit");
// ...
}
Sub-accounts are a standard pattern for factory contracts: token.myapp.near, pool.myapp.near. Each sub-account is a fully independent account.
Why Storage Staking Is Critical for Your Budget
The choice of data structure directly impacts costs. Here’s a comparison of popular collections:
| Collection |
Iteration |
Storage cost per 1000 records |
Gas per write |
LookupMap |
No |
~0.01 NEAR |
2 TGas |
UnorderedMap |
Yes |
~0.015 NEAR |
3 TGas |
Vector |
Yes |
~0.02 NEAR |
4 TGas |
Rust contracts on Near are 2-3 times more performance-efficient than JavaScript for the same logic — our tests on identical operations (1000 records, ~3 TGas vs ~8 TGas) confirm this.
Runtime and Languages
The Near VM executes WASM bytecode. Two languages are officially supported.
Rust + near-sdk-rs — the primary choice for production. The SDK provides macros like #[near_bindgen], #[init], BorshSerialize/BorshDeserialize for state serialization. Borsh (Binary Object Representation Serializer for Hashing) is a deterministic binary format mandatory for storing Near contract state.
JavaScript/TypeScript + near-sdk-js — for prototypes and simple logic. It compiles to WASM via QuickJS, with noticeably lower performance and tighter gas limits.
Example of a minimal Rust contract:
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
}
}
Cross-Contract Calls and Async Model
Near is an asynchronous sharded blockchain. A cross-contract call is not executed synchronously within the transaction — it creates a separate receipt that is processed in the next block. The result comes back in a callback. This model complicates design but allows error handling without full state rollback.
#[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) => { /* processing */ }
Err(_) => { env::panic_str("External call failed") }
}
}
}
#[private] — a macro that adds the check assert_eq!(env::current_account_id(), env::predecessor_account_id()). Without it, anyone can call the callback directly.
Build and Deploy
Tooling: cargo-near — official CLI for building, near-cli-rs (Rust-rewritten version) for deployment.
# Installation
cargo install cargo-near
cargo install near-cli-rs
# Build optimized WASM
cargo near build
# Deploy to testnet
near contract deploy myapp.testnet \
use-file ./target/near/myapp.wasm \
without-init-call \
network-config testnet \
sign-with-keychain \
send
The WASM file after building with cargo-near automatically passes through wasm-opt (Binaryen) — this reduces size by up to 30% and saves on storage.
For mainnet deployment, use a multisig account or near-cli with a hardware wallet (--useLedgerKey). Deploying without initialization leaves the contract uninitialized; the first call to new() sets the initial state.
Testing
Unit tests — standard Rust unit tests with VMContextBuilder to mock the environment (predecessor, attached_deposit, block_timestamp).
Workspaces-rs — integration tests that run a local Near sandbox and deploy real WASM. This is the de facto standard for testing cross-contract interactions:
#[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(())
}
Typical Mistakes When Migrating from EVM
- No
msg.sender as an address. In Near, env::predecessor_account_id() is a string, not 20 bytes. Comparison uses == for AccountId.
- Panic instead of revert.
env::panic_str("message") or assert!() — similar to require() in Solidity. Gas for panic is not refunded.
- Iteration over
LookupMap. LookupMap is not iterable. For iterable collections, use UnorderedMap or Vector. Choosing the right data structure affects gas.
- Gas units. 1 TGas = 10¹² gas. A simple call ~2-3 TGas, a cross-contract call +5 TGas minimum. Transaction limit is 300 TGas.
More about sub-accounts
Sub-accounts are created by deploying a contract to a name like `subdomain.main.near`. They inherit permissions from the parent account but have their own storage. For example, `token.mybank.near` is an independent contract with its own balance, not linked to `mybank.near`. This is convenient for multi-token platforms.
What's Included in Turnkey Development
- Requirements audit and language selection (Rust/JS).
- Storage model design — critical for cost (correct collection types reduce storage by 30-50%).
- Development with unit tests.
- Integration testing via workspaces.
- Deployment to testnet, verification via Near Explorer.
- Deployment to mainnet with multisig.
- Handover of contract access and documentation.
- Post-deployment support — 1 month included.
Deployment time for ready-to-go code — 4 hours. Contract development from scratch with testing: 1-5 days depending on complexity.
We guarantee compliance with Near standards (NEP-145, NEP-141) and provide full documentation. Our engineers are Near Certified Developers.
Order turnkey development — we will evaluate your project within 1 business day. Contact us for a project assessment — we will analyze requirements, propose the optimal approach, and meet deadlines without surprises.
Blockchain Infrastructure Deployment: Nodes, RPC, Indexing
Subgraph fell at 3:47 AM. By morning users saw outdated balances, transactions "hung" in the UI, support received 47 tickets in an hour. Cause: the handler in the subgraph failed on a transaction with a non-standard event log — and the entire index stopped. We have encountered such situations dozens of times. Our experience shows: blockchain infrastructure does not forgive gaps in observability. Guaranteeing uptime without multi-layered monitoring and fault-tolerant architecture is impossible. Over 8 years working with Ethereum, Polygon, and Solana, we have developed an approach that allows predictable deployment of infrastructure of any scale — from a single node to a multichain grid with dozens of subgraphs.
RPC Layer Architecture
Every dApp interaction with the blockchain goes through RPC — the JSON-RPC API provided by a node. Three options:
Managed providers — Alchemy, QuickNode, Infura, Ankr. Minimal operational costs, SLA, built-in monitoring. Limits: rate limits (Alchemy Free: 300 RU/sec), vendor lock, potential downtime during provider incidents. For most projects — the right choice at the start.
Self-owned nodes — full control, no rate limits, no third-party dependence. Cost: archive Ethereum node requires 2.5–3TB SSD, a strong server, and DevOps support. Sync from scratch on Ethereum via Geth/Nethermind — 3–7 days. Justified under high load or latency requirements.
Hybrid — self-owned node as primary, managed provider as fallback. Standard for protocols with high TVL. Proper load balancing can reduce costs by 20–30% compared to pure managed setup. Under high monthly request volume, hybrid saves significantly.
| Provider |
Strength |
Limitation |
| Alchemy |
Supernode, Enhanced APIs, webhooks |
Expensive on high-volume |
| QuickNode |
Low latency, multi-chain |
More expensive than Alchemy on basic plan |
| Infura |
Historical reliability |
Rate limits on free, one major incident halted half of DeFi |
| Ankr |
Cheap, 40+ chains |
Less stable |
How to Set Up an RPC Layer Without a Single Point of Failure?
At least two providers, DNS round-robin with health check every 5 seconds, automatic fallback when latency >500 ms. In practice, this gives 99.99% availability during any provider failure. For protocols with high TVL, we recommend a custom HA-proxy (nginx or Envoy) in front of two managed providers.
Why Is a Hybrid RPC Scheme More Cost-Effective Than Pure Managed?
At high request volumes, managed providers can be very expensive; a hybrid using a self-owned node as primary and a managed fallback cuts costs significantly without losing SLA.
Ethereum Node Clients
Execution clients: Geth (most used), Nethermind (C#, fast sync), Besu (Java, enterprise), Erigon (fastest sync, efficient archive mode ~2TB instead of 3TB).
Consensus clients (post-Merge): Lighthouse (Rust), Prysm (Go), Teku (Java), Nimbus (Nim). Each node after The Merge requires a pair of execution + consensus clients.
For DevOps: eth-docker — Docker Compose configurations for all client combinations. Setting up monitoring via Grafana + Prometheus is mandatory; a standard dashboard is available in each client's repository.
The Graph: Event Indexing
The Graph Protocol — decentralized indexing. A subgraph describes which events from which contracts to index and how to transform them into a GraphQL schema.
Subgraph structure:
-
subgraph.yaml — manifest: contract addresses, startBlock, events to handle
-
schema.graphql — GraphQL schema of entities
-
src/mapping.ts — AssemblyScript event handlers
dataSources:
- kind: ethereum
name: UniswapV3Pool
network: mainnet
source:
address: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"
abi: UniswapV3Pool
startBlock: 12370624
mapping:
eventHandlers:
- event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)
handler: handleSwap
AssemblyScript handlers — not TypeScript. No nullable types, no closures, no many standard APIs. An error in the handler stops the subgraph indexing on that transaction. Important: add try-catch for operations that can fail (e.g., store.get() for an entity that may not exist).
How to Avoid Subgraph Indexing Stops?
Graph Node logs are monitored in real-time; on hasIndexingErrors = true an alert fires and an automatic node restart (via systemd or Kubernetes). Typical downtime on error — 150–300 seconds to recover. Additionally, for production we set up a watchdog that restarts Graph Node if subgraph lag exceeds 50 blocks.
Choosing Between Hosted Service and Decentralized Network
Graph Hosted Service (free, centralized) is deprecated in favor of Subgraph Studio + Graph Network. For production: deploy on Graph Network with GRT curation signal — the subgraph gets indexers proportional to curation.
Alternatives to The Graph: Ponder (TypeScript, self-hosted, easier to debug), Envio (ultra-fast indexer, supports EVM + non-EVM), Subsquid (TypeScript, own network), Moralis Streams (managed, webhook-based). Our experience shows: for high-load projects with unique logic, Ponder or Envio are more effective — they give full control over the process and do not require GRT tokenomics.
Webhooks and Real-Time Notifications
Alchemy Webhooks and QuickNode Streams allow receiving events in real-time via HTTP webhook or WebSocket. For monitoring addresses, new transactions, mints — this is faster than polling RPC.
Tenderly — platform for monitoring and alerts. You can set up an alert for a specific contract event, balance change, function call with certain parameters. Transaction simulation via Tenderly API is invaluable for debugging.
Monitoring and Observability
Minimum monitoring stack for a protocol:
On-chain: OpenZeppelin Defender Sentinel — watches contract events, triggers webhook or Autotask when conditions are met. Forta Network — community-maintained bots detect anomalies (large withdrawals, flash loans, governance attacks).
Infrastructure: Grafana + Prometheus for nodes, Datadog or Grafana Cloud for managed metrics. Alerts on: node is 10+ blocks behind, RPC latency >500ms, subgraph lag >100 blocks.
Uptime: Better Uptime or PagerDuty on RPC endpoint and subgraph health endpoint (The Graph provides _meta { hasIndexingErrors, block { number } }).
Why Is Monitoring Without Tenderly Insufficient?
Tenderly provides transaction simulation and detailed traces — critical for debugging subgraph and smart contract errors. Forta focuses on network anomalies, not your infrastructure. The combination of Tenderly plus a custom Grafana dashboard covers 90% of incident scenarios.
Multichain Infrastructure
A protocol on 5 chains = 5 separate RPC endpoints, 5 subgraphs, 5 monitoring configs. Manageable but requires deployment automation.
For subgraph multi-network deployment: graph deploy --network mainnet, graph deploy --network arbitrum-one etc. with a unified codebase and network-specific addresses in separate config files.
Chainlink CCIP and LayerZero for cross-chain messaging require monitoring of both chains and transactions on intermediate relayers. A reorg on the source chain after a confirmed mint on the target chain is a classic bridge problem. Solution: wait for finality (on Ethereum ~15 minutes after Merge for economic finality) before confirming on the target chain.
Infrastructure Setup Process
- Audit current stack — determine chains, request volume, latency and availability requirements.
- Architecture design — select providers, load balancing, redundancy.
- Subgraph development — manifest → schema → handlers → testing on local Graph Node → deploy to testnet → mainnet.
- Monitoring configuration — Tenderly alerts, Grafana dashboard, PagerDuty integration.
- Documentation and runbook — what to do when: subgraph falls behind, RPC downtime, node desync.
- Handover to operations — team training, access transfer, first month support.
What's Included
- Deployment of managed or self-hosted Ethereum, Polygon, BNB Chain nodes
- RPC layer setup with primary/fallback and load balancing
- Subgraph development and deployment for your protocol
- Monitoring connection (Tenderly, Grafana, alerts)
- Runbook and operations documentation
- Team training (up to 4 hours online)
- 30-day support after delivery
Timeline
| Task |
Duration |
| RPC and basic monitoring setup |
1–2 weeks |
| Subgraph for one protocol |
2–4 weeks |
| Self-hosted node with monitoring |
2–3 weeks |
| Full infrastructure (multi-chain, monitoring, runbooks) |
6–10 weeks |
All projects are managed in a GitHub/GitLab repository with CI/CD; configuration code stays with you. Order infrastructure deployment — we'll show how to cut costs by 20–30% without losing reliability. Get a consultation — we'll demonstrate how we deployed infrastructure for a protocol with large TVL on Ethereum and Arbitrum. Contact us.