Skip to main content

Editorial polish is still intentionally in progress

This section is hidden from primary navigation. Translation and editorial polish are intentionally out of scope until the section is public.

RPC & Submission

This section covers how transactions are submitted via JSON-RPC, validated by the RpcHandler, and prepared for network propagation.

JSON-RPC Protocol Overview​

NEAR nodes expose a JSON-RPC 2.0 interface for external interaction. Unlike REST APIs, JSON-RPC uses a single endpoint with method names in the request body.

Default Endpoint: http://localhost:3030 (configurable)

Request Format​

{
    "jsonrpc": "2.0",
    "id": "unique-request-id",
    "method": "method_name",
    "params": { /* method-specific parameters */ }
}

Response Formats​

Success:

{
    "jsonrpc": "2.0",
    "id": "unique-request-id",
    "result": { /* method-specific result */ }
}

Error:

{
    "jsonrpc": "2.0",
    "id": "unique-request-id",
    "error": {
        "code": -32000,
        "message": "Error message",
        "data": { /* optional additional info */ }
    }
}

RPC Configuration​

Source: chain/jsonrpc/src/lib.rs

pub struct RpcConfig {
    /// Address to bind the RPC server (default: "0.0.0.0:3030")
    pub addr: tcp::ListenerAddr,

    /// Optional Prometheus metrics endpoint
    pub prometheus_addr: Option<String>,

    /// CORS allowed origins (default: ["*"])
    pub cors_allowed_origins: Vec<String>,

    /// Polling configuration for tx_status waits
    pub polling_config: RpcPollingConfig,

    /// Request size limits (default: 10MB)
    pub limits_config: RpcLimitsConfig,

    /// Enable debug RPC endpoints (default: false)
    pub enable_debug_rpc: bool,
}

Transaction Submission Methods​

NEAR provides three RPC methods for submitting transactions, each with different waiting behavior.

1. broadcast_tx_async​

Purpose: Fire-and-forget transaction submission

Submits the transaction and immediately returns the transaction hash without waiting for any confirmation.

When to use:

  • High-throughput applications that track status separately
  • When you don't need immediate confirmation
  • Reducing client latency

Request:

{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "broadcast_tx_async",
    "params": ["BASE64_ENCODED_SIGNED_TX"]
}

Response:

{
    "jsonrpc": "2.0",
    "id": "1",
    "result": "6zgh2u9DqHHiXzdy9ouTP7oGky2T4nugqzqt9wJZwNFm"
}

The result is the transaction hash (base58-encoded CryptoHash).

2. broadcast_tx_commit​

Purpose: Submit and wait for execution

Submits the transaction and waits until it's included in a final block with execution results.

When to use:

  • Simple applications that need confirmation
  • Testing and debugging
  • When you need execution results immediately

Request:

{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "broadcast_tx_commit",
    "params": ["BASE64_ENCODED_SIGNED_TX"]
}

Response (success):

{
    "jsonrpc": "2.0",
    "id": "1",
    "result": {
        "status": { "SuccessValue": "" },
        "transaction": {
            "signer_id": "sender.testnet",
            "public_key": "ed25519:...",
            "nonce": 1,
            "receiver_id": "receiver.testnet",
            "actions": [...],
            "signature": "ed25519:...",
            "hash": "6zgh2u9DqHHiXzdy9ouTP7oGky2T4nugqzqt9wJZwNFm"
        },
        "transaction_outcome": {
            "block_hash": "...",
            "id": "6zgh2u9DqHHiXzdy9ouTP7oGky2T4nugqzqt9wJZwNFm",
            "outcome": {
                "logs": [],
                "receipt_ids": ["..."],
                "gas_burnt": 424555062500,
                "tokens_burnt": "42455506250000000000",
                "executor_id": "sender.testnet",
                "status": { "SuccessReceiptId": "..." }
            }
        },
        "receipts_outcome": [...]
    }
}

Timeout: Default 10 seconds (configurable via polling_config)

3. send_tx​

Purpose: Flexible transaction submission with configurable waiting

The modern API that combines the functionality of the above methods with additional control.

Request:

{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "send_tx",
    "params": {
        "signed_tx_base64": "BASE64_ENCODED_SIGNED_TX",
        "wait_until": "INCLUDED_FINAL"
    }
}

wait_until Options:

ValueDescription
NONEDon't wait, return immediately (like broadcast_tx_async)
INCLUDEDWait until included in a block
INCLUDED_FINALWait until included in a final block
EXECUTEDWait until executed
EXECUTED_OPTIMISTICWait for execution with optimistic finality
FINALWait for final execution (like broadcast_tx_commit)

Request Parsing and Decoding​

When a transaction request arrives at the RPC layer, it goes through several parsing stages.

Step 1: JSON-RPC Dispatch​

The main RPC handler routes the request based on the method name.

Step 2: Parameter Parsing​

Multiple input formats are supported:

// Array format
{"params": ["BASE64_TX"]}

// Object format (modern)
{"params": {"signed_tx_base64": "BASE64_TX", "wait_until": "FINAL"}}

Step 3: Transaction Decoding​

The critical decoding function converts base64 to SignedTransaction:

fn decode_signed_transaction(value: String) -> Result<SignedTransaction, RpcParseError> {
    // Step 1: Base64 decode to bytes
    let bytes = near_primitives::serialize::from_base64(&value)?;

    // Step 2: Borsh deserialize to SignedTransaction
    SignedTransaction::try_from_slice(&bytes)?
}

This is where the encoding boundary happens:

  • Input: Base64 string containing Borsh bytes
  • Output: Rust SignedTransaction struct

Error Handling at the RPC Layer​

Parse Errors (Code -32700)​

Returned when the request cannot be parsed:

{
    "error": {
        "code": -32700,
        "message": "Parse error",
        "data": "Failed to decode transaction: invalid base64"
    }
}

Common causes:

  • Invalid base64 encoding
  • Corrupted borsh data
  • Wrong parameter format

Invalid Transaction Errors​

When the transaction is parseable but invalid:

{
    "error": {
        "code": -32000,
        "message": "Server error",
        "data": {
            "InvalidTransaction": {
                "InvalidNonce": {
                    "tx_nonce": 5,
                    "ak_nonce": 10
                }
            }
        }
    }
}

Error Code Reference​

CodeMeaning
-32700Parse error (invalid JSON)
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error
-32000Server error (NEAR-specific errors)

View Calls: The Read-Only Path​

Not all RPC calls go through the transaction flow. View calls are read-only function executions that bypass transactions, receipts, and state changes entirely.

Key Differences​

AspectTransactionView Call
SigningRequiredNot needed
Gas costPaid by signerFree
State changesYesNo (read-only)
Creates receiptsYesNo
Cross-contract callsYesNo
FinalityEventualImmediate
Block inclusionRequiredN/A

The Query RPC Method​

{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "query",
    "params": {
        "request_type": "call_function",
        "finality": "final",
        "account_id": "contract.near",
        "method_name": "get_status",
        "args_base64": "e30="
    }
}

Request types:

TypePurpose
view_accountGet account balance, storage, code hash
view_codeGet contract WASM code
view_stateGet contract storage key-values
view_access_keyGet specific access key info
view_access_key_listList all access keys for account
call_functionExecute read-only contract method

What's Prohibited in View Calls​

CategoryBlocked Functions
State modificationstorage_write, storage_remove
Signing contextsigner_account_id, signer_account_pk, predecessor_account_id
Gas infoprepaid_gas, used_gas
All promisespromise_create, promise_then, promise_batch_*
TransfersAny deposit (always 0 in view)

Allowed operations:

CategoryAllowed Functions
State readingstorage_read, storage_has_key
Block infoblock_height, block_timestamp, epoch_height
Account infocurrent_account_id, account_balance, account_locked_balance
Cryptosha256, keccak256, ed25519_verify

Transaction Validation​

After the RPC layer parses the transaction, it hands it off to the RpcHandler actor for validation.

The RpcHandler Actor​

The RpcHandler is a multi-threaded actor that validates transactions and manages the transaction pool.

Source: chain/client/src/rpc_handler.rs

pub struct RpcHandler {
    config: RpcHandlerConfig,

    /// The transaction mempool
    tx_pool: Arc<Mutex<ShardedTransactionPool>>,

    /// Access to chain state
    chain_store: ChainStoreAdapter,

    /// Epoch management (validators, shards)
    epoch_manager: Arc<dyn EpochManagerAdapter>,

    /// Tracks which shards this node handles
    shard_tracker: ShardTracker,

    /// Runtime for state access and tx validation
    runtime: Arc<dyn RuntimeAdapter>,

    /// Network interface for forwarding
    network_adapter: PeerManagerAdapter,
}

Validity Period Checks​

The first validation step ensures the transaction isn't expired or from a different fork.

check_transaction_validity_period(
    &self.chain_store,
    &cur_block_header,
    signed_tx.transaction.block_hash(),
    self.config.transaction_validity_period,
)

The check involves:

  1. Block Hash Lookup: Find the block referenced by transaction.block_hash
  2. Chain Membership: Verify the block is an ancestor of the current head
  3. Height Check: Ensure current_height - referenced_height < validity_period

If the referenced block is:

  • Not found: InvalidChain error
  • On a different fork: InvalidChain error
  • Too old: Expired error

Why Validity Periods?​

  1. DoS Prevention: Without expiration, old transactions could be replayed indefinitely
  2. Fork Protection: Ensures transactions are only valid on the intended chain
  3. Mempool Cleanup: Nodes can safely discard old unconfirmed transactions
  4. User Safety: Prevents very old transactions from suddenly confirming

On mainnet, transaction_validity_period is 86,400 blocks (~24 hours).

Signature Verification​

The signature is verified using the public key embedded in the transaction.

Source: core/primitives/src/transaction.rs

if !signed_tx
    .signature
    .verify(signed_tx.get_hash().as_ref(), signed_tx.transaction.public_key())
{
    return Err((InvalidTxError::InvalidSignature, signed_tx));
}

What Gets Signed?​

The signature covers the Borsh-serialized transaction (not including the signature itself):

impl Transaction {
    pub fn get_hash_and_size(&self) -> (CryptoHash, u64) {
        let bytes = borsh::to_vec(&self).expect("Failed to deserialize");
        (hash(&bytes), bytes.len() as u64)  // SHA-256 hash
    }
}

Nonce Management​

Nonces prevent replay attacks by ensuring each transaction is unique.

Nonce Rules​

  1. Strictly Increasing: tx_nonce > access_key_nonce
  2. Per Access Key: Each (account, public_key) pair has its own nonce
  3. Upper Bound: tx_nonce < current_block_height * 1_000_000

Common Nonce Issues​

"Nonce too low": The transaction's nonce has already been used. Causes:

  • Replaying an old transaction
  • Previous transaction confirmed faster than expected
  • Using stale nonce from cache

"Nonce too high": The nonce is unreasonably large. Sanity check to prevent:

  • Integer overflow issues
  • Denial of service via nonce exhaustion

Best Practices​

// Get current nonce
const accessKey = await account.connection.provider.query({
    request_type: "view_access_key",
    finality: "final",
    account_id: accountId,
    public_key: publicKey
});

// Use nonce + 1 for the transaction
const nonce = accessKey.nonce + 1;

Access Key Validation​

Access keys control what actions a transaction can perform.

Access Key Types​

Full Access Keys:

  • Can perform any action
  • Can add/remove other keys
  • Typically used for account owners

Function Call Keys:

  • Limited to calling specific methods
  • Can have an allowance (gas spending limit)
  • Used for dApp integrations

Validation Logic​

pub struct FunctionCallPermission {
    /// Gas allowance remaining (None = unlimited)
    pub allowance: Option<Balance>,

    /// Must match transaction receiver_id
    pub receiver_id: AccountId,

    /// If non-empty, method_name must be in this list
    pub method_names: Vec<String>,
}

Checks performed:

  1. Key Exists: The public key must be registered on the signer account
  2. Permission Type: Full access required for non-FunctionCall actions
  3. Receiver Match: Function call keys can only call their designated receiver
  4. Method Match: If method_names is non-empty, the called method must be listed
  5. Allowance: Must have sufficient remaining allowance for gas

Error Types​

ErrorDescription
AccessKeyNotFoundKey doesn't exist on the account
RequiresFullAccessTrying to use function call key for non-call action
ReceiverMismatchReceiver doesn't match function call key
MethodNameMismatchMethod not in allowed list
NotEnoughAllowanceInsufficient allowance
DepositWithFunctionCallFunction call keys can't send deposits

Gas and Balance Checks​

Before a transaction can be accepted, the signer must have sufficient resources.

Gas Calculation​

Each action has associated gas costs defined in the protocol parameters:

total_gas = sum(action_gas) + receipt_creation_gas

Balance Requirements​

The signer must have:

balance >= gas_price * total_gas + sum(deposits) + storage_stake_required

Where:

  • gas_price: Current price of gas (set by protocol)
  • deposits: Sum of all Transfer and FunctionCall deposits
  • storage_stake_required: Stake needed for account storage

Validation Errors​

ErrorDescription
NotEnoughBalanceSigner doesn't have enough NEAR
LackBalanceForStateCan't cover storage stake
CostOverflowInteger overflow during calculation

Shard Routing Logic​

NEAR shards the state by account. Each transaction must be routed to the correct shard.

Shard Assignment​

let shard_uid = shard_layout.account_id_to_shard_uid(signed_tx.transaction.signer_id());
let shard_id = shard_uid.shard_id();

The signer's account determines which shard processes the transaction initially. If the receiver is on a different shard, cross-shard receipts handle the communication.

Routing Decision​

The RpcHandler determines:

  1. Which shard the transaction belongs to (based on signer_id)
  2. Whether this node tracks that shard
  3. Whether this node is the chunk producer for that shard
  4. If not, forward to the appropriate chunk producer via P2P