Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/near/near-api-ts/llms.txt

Use this file to discover all available pages before exploring further.

This guide covers everything you need to know about sending transactions on NEAR, from simple transfers to complex multi-action transactions.

Prerequisites

Install the NEAR API TypeScript library:
npm install near-api-ts

Overview

Sending transactions on NEAR involves three main steps:
  1. Create a signer - Manages your keys and signs transactions
  2. Define actions - Specify what the transaction should do
  3. Execute the transaction - Send it to the network

Setting Up a Signer

A signer handles transaction signing using your private keys. The library provides MemorySigner for most use cases.
1

Create a client

import { createTestnetClient } from 'near-api-ts';

const client = createTestnetClient();
2

Create a key service

import { createMemoryKeyService } from 'near-api-ts';

const keyService = createMemoryKeyService({
  keySource: {
    privateKey: 'ed25519:your-private-key-here'
  }
});
3

Create the signer

import { createMemorySigner } from 'near-api-ts';

const signer = createMemorySigner({
  signerAccountId: 'your-account.testnet',
  client,
  keyService
});

Sending NEAR Tokens

Simple Transfer

The most common transaction is transferring NEAR tokens:
import { createTestnetClient, createMemoryKeyService, createMemorySigner, transfer, near } from 'near-api-ts';

const client = createTestnetClient();
const keyService = createMemoryKeyService({
  keySource: { privateKey: 'ed25519:your-private-key' }
});
const signer = createMemorySigner({
  signerAccountId: 'sender.testnet',
  client,
  keyService
});

// Transfer 1 NEAR
const result = await signer.executeTransaction({
  intent: {
    action: transfer({ amount: near('1') }),
    receiverAccountId: 'receiver.testnet'
  }
});

console.log('Transaction hash:', result.transactionOutcome.id);
console.log('Status:', result.transactionOutcome.outcome.status);

Transfer with YoctoNEAR

You can also specify amounts in yoctoNEAR (1 NEAR = 10²⁴ yoctoNEAR):
import { transfer, yoctoNear } from 'near-api-ts';

// Transfer exactly 1,000,000 yoctoNEAR
await signer.executeTransaction({
  intent: {
    action: transfer({ amount: yoctoNear('1000000') }),
    receiverAccountId: 'receiver.testnet'
  }
});

// Or use the object syntax
await signer.executeTransaction({
  intent: {
    action: transfer({ amount: { yoctoNear: '1000000' } }),
    receiverAccountId: 'receiver.testnet'
  }
});

Working with NEAR Tokens

The NearToken type provides methods for token arithmetic:
import { near, transfer } from 'near-api-ts';

// Create token amounts
const amount1 = near('5');
const amount2 = near('2.5');

// Perform arithmetic
const total = amount1.add(amount2);  // 7.5 NEAR
const remaining = amount1.sub(amount2);  // 2.5 NEAR

// Comparisons
if (amount1.gt(amount2)) {
  console.log('amount1 is greater');
}

// Access values
console.log('NEAR:', total.near);  // 7.5
console.log('YoctoNEAR:', total.yoctoNear);  // 7500000000000000000000000

// Use in transfer
await signer.executeTransaction({
  intent: {
    action: transfer({ amount: total }),
    receiverAccountId: 'receiver.testnet'
  }
});

Transaction Error Handling

Using Try-Catch

Handle errors with standard try-catch:
import { isNatError, transfer, near } from 'near-api-ts';

try {
  await signer.executeTransaction({
    intent: {
      action: transfer({ amount: near('1000') }),
      receiverAccountId: 'receiver.testnet'
    }
  });
  console.log('Transfer successful');
} catch (error) {
  if (isNatError(error, 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Signer.Balance.TooLow')) {
    console.log('Insufficient balance');
    console.log('Required:', error.context.transactionCost);
  } else if (isNatError(error, 'MemorySigner.ExecuteTransaction.Rpc.Account.NotFound')) {
    console.log('Receiver account does not exist');
  } else {
    console.error('Transaction failed:', error.kind);
  }
}

Using Safe Variants

For more explicit error handling without exceptions:
const result = await signer.safeExecuteTransaction({
  intent: {
    action: transfer({ amount: near('1') }),
    receiverAccountId: 'receiver.testnet'
  }
});

if (result.ok) {
  console.log('Success:', result.value.transactionOutcome.id);
} else {
  console.error('Failed:', result.error.kind);
  // Handle the error
}

Multi-Action Transactions

NEAR allows multiple actions in a single transaction:
import { createAccount, transfer, addFullAccessKey, near, randomEd25519KeyPair } from 'near-api-ts';

// Generate a new key pair for the new account
const newKeyPair = randomEd25519KeyPair();

// Create account with multiple actions
await signer.executeTransaction({
  intent: {
    actions: [
      createAccount(),
      transfer({ amount: near('5') }),
      addFullAccessKey({ publicKey: newKeyPair.publicKey })
    ],
    receiverAccountId: 'newaccount.sender.testnet'
  }
});

console.log('New account created with 5 NEAR');
console.log('New account private key:', newKeyPair.privateKey);

Function Call Transactions

Calling Contract Methods

Call smart contract methods with function call actions:
import { functionCall, teraGas, near } from 'near-api-ts';

// Call a contract method
await signer.executeTransaction({
  intent: {
    action: functionCall({
      functionName: 'set_status',
      functionArgs: {
        message: 'Hello, NEAR!'
      },
      gasLimit: teraGas('30'),
      attachedDeposit: near('0')  // No deposit
    }),
    receiverAccountId: 'contract.testnet'
  }
});

With Attached Deposit

Many contract methods require NEAR tokens to be attached:
import { functionCall, teraGas, near } from 'near-api-ts';

// Mint an NFT with 0.1 NEAR attached
await signer.executeTransaction({
  intent: {
    action: functionCall({
      functionName: 'nft_mint',
      functionArgs: {
        token_id: '123',
        metadata: {
          title: 'My NFT',
          description: 'An example NFT'
        },
        receiver_id: 'owner.testnet'
      },
      gasLimit: teraGas('50'),
      attachedDeposit: near('0.1')
    }),
    receiverAccountId: 'nft-contract.testnet'
  }
});

Custom Argument Serialization

For non-JSON contracts, provide a custom serializer:
import { functionCall, teraGas, toJsonBytes } from 'near-api-ts';

await signer.executeTransaction({
  intent: {
    action: functionCall({
      functionName: 'add_item',
      functionArgs: { itemId: 42, itemName: 'Widget' },
      gasLimit: teraGas('30'),
      options: {
        serializeArgs: ({ functionArgs }) => {
          // Convert camelCase to snake_case for contract
          return toJsonBytes({
            item_id: functionArgs.itemId,
            item_name: functionArgs.itemName
          });
        }
      }
    }),
    receiverAccountId: 'contract.testnet'
  }
});

Working with Gas

Gas Amounts

Gas can be specified in different units:
import { gas, teraGas } from 'near-api-ts';

// Using teraGas (most common)
const gasLimit1 = teraGas('30');  // 30 TGas

// Using raw gas units
const gasLimit2 = gas('30000000000000');  // 30 TGas in gas units

// Gas arithmetic
const total = teraGas('20').add(teraGas('10'));  // 30 TGas
const remaining = teraGas('50').sub(teraGas('20'));  // 30 TGas

// Comparisons
if (teraGas('50').gt(teraGas('30'))) {
  console.log('50 TGas is greater than 30 TGas');
}

Gas Estimation

Different operations require different amounts of gas:
  • Simple transfers: ~0.3 TGas
  • Function calls: 30-50 TGas (depends on contract)
  • Complex operations: 100-200 TGas
  • Cross-contract calls: 100-300 TGas
import { functionCall, teraGas } from 'near-api-ts';

// Conservative gas limit for contract calls
await signer.executeTransaction({
  intent: {
    action: functionCall({
      functionName: 'complex_operation',
      functionArgs: {},
      gasLimit: teraGas('100')  // Generous limit
    }),
    receiverAccountId: 'contract.testnet'
  }
});

Advanced Patterns

Transaction with Multiple Function Calls

import { functionCall, teraGas, near } from 'near-api-ts';

// Batch operations in one transaction
await signer.executeTransaction({
  intent: {
    actions: [
      functionCall({
        functionName: 'operation_1',
        functionArgs: { data: 'first' },
        gasLimit: teraGas('30')
      }),
      functionCall({
        functionName: 'operation_2',
        functionArgs: { data: 'second' },
        gasLimit: teraGas('30')
      }),
      functionCall({
        functionName: 'operation_3',
        functionArgs: { data: 'third' },
        gasLimit: teraGas('30')
      })
    ],
    receiverAccountId: 'contract.testnet'
  }
});

Signing Without Executing

Sign a transaction without sending it to the network:
import { transfer, near } from 'near-api-ts';

const signedTransaction = await signer.signTransaction({
  intent: {
    action: transfer({ amount: near('1') }),
    receiverAccountId: 'receiver.testnet'
  }
});

// Later, send the signed transaction
const result = await client.sendSignedTransaction({
  signedTransaction
});

Transaction Retry Logic

import { transfer, near } from 'near-api-ts';

async function sendWithRetry(maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const result = await signer.executeTransaction({
        intent: {
          action: transfer({ amount: near('1') }),
          receiverAccountId: 'receiver.testnet'
        }
      });
      return result;
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      console.log(`Attempt ${attempt + 1} failed, retrying...`);
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}

const result = await sendWithRetry();
console.log('Transaction successful:', result.transactionOutcome.id);

Complete Example: Payment Flow

Here’s a complete example of a payment system with error handling:
import { 
  createTestnetClient,
  createMemoryKeyService,
  createMemorySigner,
  transfer,
  near,
  isNatError
} from 'near-api-ts';

interface PaymentResult {
  success: boolean;
  transactionId?: string;
  error?: string;
}

async function sendPayment(
  fromAccount: string,
  privateKey: string,
  toAccount: string,
  amount: string
): Promise<PaymentResult> {
  try {
    // Setup
    const client = createTestnetClient();
    const keyService = createMemoryKeyService({
      keySource: { privateKey }
    });
    const signer = createMemorySigner({
      signerAccountId: fromAccount,
      client,
      keyService
    });

    // Validate amount
    const nearAmount = near(amount);
    if (nearAmount.near <= 0) {
      return {
        success: false,
        error: 'Amount must be positive'
      };
    }

    // Execute transaction
    const result = await signer.executeTransaction({
      intent: {
        action: transfer({ amount: nearAmount }),
        receiverAccountId: toAccount
      }
    });

    return {
      success: true,
      transactionId: result.transactionOutcome.id
    };
  } catch (error) {
    // Handle specific errors
    if (isNatError(error, 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Signer.Balance.TooLow')) {
      return {
        success: false,
        error: 'Insufficient balance'
      };
    } else if (isNatError(error, 'MemorySigner.ExecuteTransaction.Rpc.Account.NotFound')) {
      return {
        success: false,
        error: 'Receiver account not found'
      };
    } else {
      return {
        success: false,
        error: 'Transaction failed'
      };
    }
  }
}

// Usage
const result = await sendPayment(
  'sender.testnet',
  'ed25519:your-private-key',
  'receiver.testnet',
  '5.5'
);

if (result.success) {
  console.log('Payment successful!');
  console.log('Transaction ID:', result.transactionId);
} else {
  console.error('Payment failed:', result.error);
}

Best Practices

Always Validate Inputs

// Validate account IDs
if (!receiverAccountId.includes('.')) {
  throw new Error('Invalid account ID format');
}

// Validate amounts
const amount = near(userInput);
if (amount.near <= 0) {
  throw new Error('Amount must be positive');
}

Use Appropriate Gas Limits

// Don't use excessive gas
const gasLimit = teraGas('30');  // Good for most operations

// Avoid
const gasLimit = teraGas('300');  // Unnecessarily high

Handle All Error Cases

try {
  await signer.executeTransaction({ intent });
} catch (error) {
  if (isNatError(error, 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Signer.Balance.TooLow')) {
    // Handle insufficient balance
  } else if (isNatError(error, 'MemorySigner.ExecuteTransaction.Rpc.Account.NotFound')) {
    // Handle account not found
  } else if (isNatError(error, 'MemorySigner.ExecuteTransaction.Rpc.Transaction.Expired')) {
    // Handle expired transaction
  } else {
    // Handle unknown errors
  }
}

Store Transaction Receipts

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

// Store important transaction data
const receipt = {
  transactionId: result.transactionOutcome.id,
  blockHash: result.transactionOutcome.blockHash,
  status: result.transactionOutcome.outcome.status,
  gasUsed: result.transactionOutcome.outcome.gasBurnt,
  timestamp: new Date().toISOString()
};

console.log('Transaction receipt:', receipt);

Next Steps