Skip to main content

Overview

A Transaction in NEAR represents a single unit of work that modifies the blockchain state. Every transaction consists of metadata (signer, receiver, nonce, block hash) and one or more actions to execute.

Transaction Structure

Transaction Type

type Transaction = {
  signerAccountId: AccountId;
  signerPublicKey: PublicKey;
  receiverAccountId: AccountId;
  nonce: Nonce;
  blockHash: BlockHash;
} & (
  | { action: Action; actions?: never }    // Single action
  | { action?: never; actions: Action[] }  // Multiple actions
);

Transaction Fields

The account that signs and pays for the transaction.
signerAccountId: 'alice.near'
The public key of the access key used to sign the transaction. Must correspond to an access key on the signer account.
signerPublicKey: 'ed25519:5FNQFvXMECbW...'
The account that receives and processes the transaction. For transfers, this is the recipient. For contract calls, this is the contract.
receiverAccountId: 'bob.near'
A unique number that must be greater than the current nonce for the access key. Prevents replay attacks.
nonce: 12345678
Hash of a recent block (within ~2 minutes). Ensures transaction is processed within a time window.
blockHash: '4hfJMGGE8NQ9xvK3kD3MFqF7...'

Transaction Intent

For simpler transaction construction, use TransactionIntent:
type TransactionIntent = {
  receiverAccountId: AccountId;
} & (
  | { action: Action; actions?: never }
  | { action?: never; actions: Action[] }
);
The Signer automatically fills in signerAccountId, signerPublicKey, nonce, and blockHash when you provide a TransactionIntent.
const intent: TransactionIntent = {
  receiverAccountId: 'bob.near',
  action: transfer({ amount: near('1') })
};

// Signer converts intent → Transaction → SignedTransaction
await signer.executeTransaction({ intent });

Actions

Actions are the operations performed by a transaction. NEAR supports nine action types:

Action Type Definition

type Action =
  | CreateAccountAction
  | TransferAction
  | AddFullAccessKeyAction
  | AddFunctionCallKeyAction
  | DeployContractAction
  | FunctionCallAction
  | StakeAction
  | DeleteKeyAction
  | DeleteAccountAction;

Transfer Action

Transfer NEAR tokens from signer to receiver.
import { transfer, near, yoctoNear } from '@near-api/client';

// Transfer 1 NEAR
const action1 = transfer({ amount: near('1') });

// Transfer 500 milliNEAR (0.5 NEAR)
const action2 = transfer({ amount: near('0.5') });

// Transfer exact yoctoNEAR (1 NEAR = 10^24 yoctoNEAR)
const action3 = transfer({ amount: yoctoNear('1000000000000000000000000') });
Type definition:
type TransferAction = {
  actionType: 'Transfer';
  amount: NearTokenArgs;  // String or bigint in yoctoNEAR
};

Function Call Action

Call a contract method, optionally with arguments, gas, and attached deposit.
import { functionCall, teraGas, gas, near } from '@near-api/client';

// Simple call without arguments
const action1 = functionCall({
  functionName: 'my_method',
  gasLimit: teraGas('30')
});

// With JSON arguments (auto-serialized)
const action2 = functionCall({
  functionName: 'transfer',
  functionArgs: {
    receiver_id: 'bob.near',
    amount: '1000000'
  },
  gasLimit: teraGas('30'),
  attachedDeposit: near('0')
});

// Custom serialization
const action3 = functionCall({
  functionName: 'custom_method',
  functionArgs: myCustomData,
  gasLimit: gas('100000000000000'),
  options: {
    serializeArgs: ({ functionArgs }) => {
      return myCustomSerializer(functionArgs);
    }
  }
});
Type definition:
type FunctionCallAction = {
  actionType: 'FunctionCall';
  functionName: string;
  functionArgs: Uint8Array;       // Serialized arguments
  gasLimit: NearGasArgs;          // String or bigint in gas units
  attachedDeposit?: NearTokenArgs; // Optional NEAR deposit
};
Function arguments are automatically JSON-serialized unless you provide a custom serializeArgs function.

Add Full Access Key

Add a full access key that can perform any action on the account.
import { addFullAccessKey } from '@near-api/client';

const action = addFullAccessKey({
  publicKey: 'ed25519:5FNQFvXMECbW...'
});
Type definition:
type AddFullAccessKeyAction = {
  actionType: 'AddKey';
  accessType: 'FullAccess';
  publicKey: PublicKey;
};

Security Warning

Full access keys can perform ANY action on the account, including transferring all funds and deleting the account. Use with extreme caution.

Add Function Call Key

Add a limited access key that can only call specific contract methods.
import { addFunctionCallKey, near } from '@near-api/client';

// Limited to specific contract
const action1 = addFunctionCallKey({
  publicKey: 'ed25519:5FNQFvXMECbW...',
  contractAccountId: 'contract.near'
});

// Limited to specific methods
const action2 = addFunctionCallKey({
  publicKey: 'ed25519:5FNQFvXMECbW...',
  contractAccountId: 'contract.near',
  allowedFunctions: ['method1', 'method2']
});

// With gas budget (allowance)
const action3 = addFunctionCallKey({
  publicKey: 'ed25519:5FNQFvXMECbW...',
  contractAccountId: 'contract.near',
  gasBudget: near('0.25')  // 0.25 NEAR allowance
});
Type definition:
type AddFunctionCallKeyAction = {
  actionType: 'AddKey';
  accessType: 'FunctionCall';
  publicKey: PublicKey;
  contractAccountId: AccountId;
  gasBudget?: NearTokenArgs;
  allowedFunctions?: string[];
};

Deploy Contract

Deploy WebAssembly contract code to an account.
import { deployContract } from '@near-api/client';
import { readFile } from 'fs/promises';

const wasmCode = await readFile('./contract.wasm');

const action = deployContract({
  code: wasmCode
});
Type definition:
type DeployContractAction = {
  actionType: 'DeployContract';
  code: Uint8Array;  // WebAssembly bytecode
};

Stake

Stake NEAR tokens to become a validator.
import { stake, near } from '@near-api/client';

const action = stake({
  amount: near('1000'),  // Minimum varies by network
  publicKey: 'ed25519:5FNQFvXMECbW...'
});
Type definition:
type StakeAction = {
  actionType: 'Stake';
  amount: NearTokenArgs;
  publicKey: PublicKey;
};

Delete Key

Remove an access key from the account.
import { deleteKey } from '@near-api/client';

const action = deleteKey({
  publicKey: 'ed25519:5FNQFvXMECbW...'
});
Type definition:
type DeleteKeyAction = {
  actionType: 'DeleteKey';
  publicKey: PublicKey;
};

Delete Account

Delete the account and transfer remaining balance to a beneficiary.
import { deleteAccount } from '@near-api/client';

const action = deleteAccount({
  beneficiaryAccountId: 'alice.near'
});
Type definition:
type DeleteAccountAction = {
  actionType: 'DeleteAccount';
  beneficiaryAccountId: AccountId;
};

Create Account

Create a new account (typically used with subaccounts).
import { createAccount } from '@near-api/client';

const action = createAccount();
Type definition:
type CreateAccountAction = {
  actionType: 'CreateAccount';
};
Creating an account requires the receiver to be a subaccount of the signer. For example, alice.near can create bob.alice.near.

Signed Transactions

After signing, a transaction becomes a SignedTransaction:
type SignedTransaction = {
  transaction: Transaction;
  transactionHash: CryptoHash;
  signature: Signature;
};

Signing Process

// 1. Create transaction intent
const intent = {
  receiverAccountId: 'bob.near',
  action: transfer({ amount: near('1') })
};

// 2. Signer builds full transaction
// - Fetches current nonce
// - Gets recent block hash
// - Adds signer account and public key

// 3. KeyService signs the transaction
const signedTx = await signer.signTransaction({ intent });

// 4. Result contains transaction, hash, and signature
console.log(signedTx.transaction);      // Full transaction
console.log(signedTx.transactionHash);  // Base58 hash
console.log(signedTx.signature);        // Base58 signature

Multiple Actions

A transaction can include multiple actions that execute atomically:
import { transfer, functionCall, near, teraGas } from '@near-api/client';

await signer.executeTransaction({
  intent: {
    receiverAccountId: 'contract.near',
    actions: [
      // First: transfer 0.1 NEAR
      transfer({ amount: near('0.1') }),
      // Then: call deposit function
      functionCall({
        functionName: 'deposit',
        gasLimit: teraGas('30')
      })
    ]
  }
});

Atomic Execution

All actions in a transaction execute atomically. If any action fails, the entire transaction is reverted.

Action Order Matters

Actions execute in the order specified:
// ✅ Correct: Create account, then add key
actions: [
  createAccount(),
  addFullAccessKey({ publicKey: '...' }),
  transfer({ amount: near('1') })
]

// ❌ Wrong: Can't add key to non-existent account
actions: [
  addFullAccessKey({ publicKey: '...' }),
  createAccount()
]

Complete Transaction Flow

Here’s the complete flow from intent to execution:
import {
  createTestnetClient,
  createMemoryKeyService,
  createMemorySigner,
  transfer,
  near
} from '@near-api/client';

// 1. Setup
const client = createTestnetClient();
const keyService = createMemoryKeyService({
  keySource: { privateKey: 'ed25519:...' }
});
const signer = createMemorySigner({
  signerAccountId: 'alice.testnet',
  client,
  keyService
});

// 2. Define intent
const intent = {
  receiverAccountId: 'bob.testnet',
  action: transfer({ amount: near('1') })
};

// 3. Execute (internally performs all steps)
const result = await signer.safeExecuteTransaction({ intent });

if (result.ok) {
  const txResult = result.value;
  
  // Transaction outcome
  console.log(txResult.transaction_outcome.outcome.status);
  
  // Receipt outcomes
  for (const receipt of txResult.receipts_outcome) {
    console.log(receipt.outcome.status);
  }
  
  // Transaction hash
  console.log(txResult.transaction.hash);
}

Internal Steps

When you call executeTransaction, the signer:
  1. Validates intent: Checks schema and arguments
  2. Fetches access keys: Gets account access keys from RPC
  3. Selects signing key: Chooses appropriate key from key pool
  4. Gets nonce: Fetches and increments nonce for the key
  5. Gets block hash: Retrieves recent block hash (cached)
  6. Builds transaction: Constructs full transaction object
  7. Signs transaction: Uses KeyService to create signature
  8. Submits transaction: Sends to network via Client
  9. Returns result: Transaction outcome or error

Native Transaction Format

Internally, transactions are encoded in a binary format for the blockchain:
type NativeTransaction = {
  signerId: AccountId;
  publicKey: NativePublicKey;
  actions: NativeAction[];
  receiverId: AccountId;
  nonce: bigint;
  blockHash: Uint8Array;
};
The library handles conversion between the developer-friendly types and the binary format using BORSH serialization.

Token and Gas Helpers

The library provides convenient helpers for working with NEAR tokens and gas:

NEAR Tokens

import { near, yoctoNear, nearToken } from '@near-api/client';

// Human-readable NEAR amounts
near('1')       // 1 NEAR
near('0.5')     // 0.5 NEAR
near('10.25')   // 10.25 NEAR

// Exact yoctoNEAR (1 NEAR = 10^24 yoctoNEAR)
yoctoNear('1000000000000000000000000')  // 1 NEAR

// Generic token (accepts both formats)
nearToken('1')
nearToken({ near: '1' })
nearToken({ yoctoNear: '1000000000000000000000000' })

Gas

import { gas, teraGas, nearGas } from '@near-api/client';

// TGas (1 TGas = 10^12 gas)
teraGas('30')   // 30 TGas (common for function calls)
teraGas('300')  // 300 TGas (maximum per transaction)

// Exact gas units
gas('100000000000000')

// Generic gas
nearGas('30')
nearGas({ teraGas: '30' })
nearGas({ gas: '30000000000000' })
Common gas amounts:
  • Simple transfers: 0 TGas (no execution)
  • View calls: 1-5 TGas
  • Function calls: 30-100 TGas
  • Complex operations: 100-200 TGas
  • Maximum per transaction: 300 TGas

Transaction Results

A successful transaction returns comprehensive result data:
type SendSignedTransactionOutput = {
  transaction: {
    hash: CryptoHash;
    signer_id: AccountId;
    receiver_id: AccountId;
    // ... other fields
  };
  transaction_outcome: {
    outcome: {
      status: ExecutionStatus;
      receipt_ids: string[];
      gas_burnt: number;
      tokens_burnt: string;
      // ... other fields
    };
  };
  receipts_outcome: Array<{
    outcome: {
      status: ExecutionStatus;
      receipt_ids: string[];
      gas_burnt: number;
      tokens_burnt: string;
      // ... other fields
    };
  }>;
};

Checking Transaction Status

const result = await signer.executeTransaction({ intent });

if (result.ok) {
  const { transaction_outcome } = result.value;
  
  if ('SuccessValue' in transaction_outcome.outcome.status) {
    console.log('Success!');
  } else if ('Failure' in transaction_outcome.outcome.status) {
    console.log('Failed:', transaction_outcome.outcome.status.Failure);
  }
}

Best Practices

Use Safe Action Creators

Always use the safe variants of action creators (safeTransfer, safeFunctionCall) in production to catch validation errors early.

Check Transaction Results

Always verify the transaction status in the result. A successful RPC response doesn’t guarantee the transaction executed successfully.

Handle Nonce Conflicts

Use a Signer instead of manually managing nonces. The Signer’s TaskQueue prevents nonce conflicts in concurrent scenarios.

Set Appropriate Gas Limits

Set gas limits based on actual function complexity. Too low causes failure; too high wastes tokens (only burnt gas is charged).

Use Recent Block Hashes

Block hashes expire after ~2 minutes. The Client’s cache handles this automatically, but manual transaction construction should fetch fresh hashes.