Skip to content

Sending a Transfer

Overview

A TVM↔TVM transfer allows sending tokens between TVM networks (e.g., TON ↔ Tetra L2). The process includes:

  1. Build Payload — get transaction data via API
  2. Send Transaction — send a transaction in TVM (transfer via Proxy)
  3. Get Transfer ID — find the hash of the transaction with the event on the Proxy contract
  4. Request Proof (non-credit) — get transaction proof via API
  5. Deploy Event Contract (non-credit) — deploy the Event contract in the destination network

Verification Mechanism (Trustless)

For TVM↔TVM, verification is performed without relay nodes, through cryptographic transaction proof:

  • The Proxy contract in the source network emits a TvmTvmNative or TvmTvmAlien event
  • The API generates a Merkle proof of the transaction (txBlockProof and txProof fields)
  • The Event contract in the destination network verifies the proof via TransactionCheckerLiteClient
  • After successful verification, the Proxy in the destination network mints/unlocks tokens

Credit vs Non-credit

Credit (useCredit: true)Non-credit (useCredit: false)
Who deploys EventGas Credit Backend automaticallyUser manually (Steps 4–5)
Proof neededNo (Backend handles it)Yes (via API)
Number of transactions1 (send)2 (send + deployEvent)
Gas in destinationPaid by Gas Credit BackendPaid by user
SpeedFasterDepends on user

Recommendation

For most cases, use useCredit: true (default value). Non-credit is needed when you want to control the Event contract deployment process yourself.

Token Types

TypeDescription
NativeA token originally created in the given network (jetton / TIP-3). During a transfer it is locked in the Proxy contract of the source network, and a corresponding Alien token is created in the destination network.
AlienA wrapped representation of a token from another network. Minted upon an incoming transfer, backed by locked Native tokens in the source network. Burned upon a reverse transfer.

API Base URL

EnvironmentBase URL
Productionhttps://tetra-history-api.chainconnect.com/v2
Testnethttps://history-api-test.chainconnect.com/v2

Step 1: Preparing the Transfer (Build Payload)

Goal: Get the payload for sending a transaction to the blockchain

API Endpoint

POST {BASE_URL}/payload/build
Content-Type: application/json

Request Parameters

typescript
interface TransferPayloadRequest {
  fromChainId: number;        // Source network chain ID
  toChainId: number;          // Destination network chain ID
  tokenAddress: string;       // Token address in source network (format `0:...`)
  recipientAddress: string;   // Recipient address in destination network
  amount: string;             // Amount in nano-units (integer string)
  senderAddress: string;      // Sender address in source network

  useCredit?: boolean;        // `true` = Credit Backend pays gas in destination. Default: `true`
  remainingGasTo: string;     // Where to return remaining gas (required for TVM→TVM)
  payload?: string;           // Additional payload
  callback?: CallbackRequest; // Callback
  tokenBalance?: {            // For native currency (wrapped native token)
    nativeTokenAmount: string; // Native currency balance (for wrapped native token transfers)
    wrappedNativeTokenAmount: string; // Wrapped native balance (for wrapped native token transfers)
  };
}
ParameterTypeRequiredDescription
fromChainIdnumberSource network chain ID
toChainIdnumberDestination network chain ID
tokenAddressstringToken address in source network (format 0:...)
recipientAddressstringRecipient address in destination network
amountstringAmount in nano-units (integer string, e.g. "1000000" for 1 USDT with 6 decimals)
senderAddressstringSender address in source network
useCreditbooleantrue = Credit Backend pays gas in destination network. Default: true
remainingGasTostringWhere to return remaining gas. Required for TVM→TVM
tokenBalance.nativeTokenAmountstring⚠️Native currency (gas token) balance. Required when sending the gas token
tokenBalance.wrappedNativeTokenAmountstring⚠️Wrapped gas token balance (e.g., wTON). Required when sending the gas token

Request Example

bash
curl -X POST '{BASE_URL}/payload/build' \
  -H 'Content-Type: application/json' \
  -d '{
    "fromChainId": -239,
    "toChainId": 2000,
    "tokenAddress": "0:1111...aaaa",
    "recipientAddress": "0:2222...bbbb",
    "amount": "1000000",
    "senderAddress": "0:3333...cccc",
    "useCredit": false,
    "remainingGasTo": "0:3333...cccc"
  }'

Response Example

json
{
  "transferKind": "TvmToTvm",
  "tokensMeta": {
    "targetToken": {
      "tokenType": "Alien",
      "isDeployed": true,
      "address": "0:4444...dddd"
    },
    "sourceTokenType": "Native"
  },
  "tokenAmount": "999000",
  "feesMeta": {
    "amount": "1000",
    "numerator": "1",
    "denominator": "1000"
  },
  "gasEstimateAmount": "155113636",
  "abiMeta": {
    "tx": "te6ccgEBBwEA6AABiw/X8c8...",
    "executionAddress": "0:5555...eeee",
    "attachedValue": "155113636",
    "abiMethod": "transfer",
    "abi": "...",
    "params": "{...}"
  },
  "trackingMeta": {
    "sourceProxy": "0:6666...ffff",
    "targetProxy": "0:7777...0000",
    "targetConfiguration": "0:8888...1111"
  },
  "payload": null
}

Response Fields

FieldDescriptionUsage
transferKindTransfer type (TvmToTvm)Direction confirmation
tokenAmountFinal amount after fee deductionAmount the recipient will receive
feesMeta.amountBridge fee amountFee information
abiMeta.executionAddressContract address for the transactionTVM transaction recipient (Step 2)
abiMeta.txFull call BOC (base64)Ready body for TonConnect (Step 2)
abiMeta.abiContract ABI (JSON string)For calling via TVM SDK (Step 2)
abiMeta.abiMethodContract method (transfer)Method name for the call (Step 2)
abiMeta.paramsMethod parameters (JSON string)Parameters for TVM SDK call (Step 2)
abiMeta.attachedValueAmount of nanoTON to sendTVM transaction gas (Step 2)
trackingMeta.sourceProxyProxy contract addressFor getting Transfer ID (Step 3)
trackingMeta.targetConfigurationEventConfiguration address in destinationFor non-credit: used in deployEvent (Step 5)

Step 2: Sending the Transaction to Blockchain

Goal: Execute the transaction and get the Transaction Hash

What You'll Need from Step 1

ParameterDescriptionUsage
abiMeta.txFull transfer call BOCFor TonConnect — ready message body
abiMeta.abiContract ABI (JSON)For TVM SDK — creating Contract
abiMeta.abiMethod"transfer"For TVM SDK — method name
abiMeta.paramsMethod parameters (JSON)For TVM SDK — call arguments
abiMeta.executionAddressContract addressTransaction recipient
abiMeta.attachedValueGas in nanoTONTransaction amount

Sending the Transaction

typescript
import TonConnectUI from '@tonconnect/ui';
import { Cell } from '@ton/core';

interface TransferPayloadResponse {
  abiMeta: {
    tx: string;              // BOC base64
    executionAddress: string; // Contract address
    attachedValue: string;   // nanoTON
    abiMethod: string;
  };
  trackingMeta: {
    sourceProxy: string;
    targetProxy: string;
    targetConfiguration: string;
  };
}

async function sendTransfer(
  tonConnectUI: TonConnectUI,
  payloadResponse: TransferPayloadResponse
): Promise<string> {
  const { abiMeta } = payloadResponse;

  console.log(`Sending ${abiMeta.abiMethod} transaction...`);
  console.log('Contract address:', abiMeta.executionAddress);
  console.log('Gas:', abiMeta.attachedValue, 'nanoTON');

  // Build the transaction
  // abiMeta.tx — ready BOC with encoded transfer call,
  // including token amount and recipient address.
  // abiMeta.attachedValue — gas (nanoTON), not the token amount.
  const transaction = {
    validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes
    messages: [
      {
        address: abiMeta.executionAddress,  // Contract address
        amount: abiMeta.attachedValue,      // Gas (nanoTON)
        payload: abiMeta.tx,                // BOC base64 from API
      },
    ],
  };

  // Send via TonConnect
  const result = await tonConnectUI.sendTransaction(transaction);

  // TonConnect returns BOC (serialized message), not a transaction hash.
  // Extract message hash from BOC — it's used in TonAPI for trace lookup.
  const messageHash = Cell.fromBase64(result.boc).hash().toString('hex');
  console.log('Transaction sent, message hash:', messageHash);

  return messageHash;
}
typescript
import { Address, ProviderRpcClient } from 'everscale-inpage-provider';

interface TransferPayloadResponse {
  abiMeta: {
    tx: string;              // BOC base64 (for TonConnect)
    abi: string;             // Contract ABI (JSON string)
    abiMethod: string;       // Contract method
    params: string;          // Method parameters (JSON string)
    executionAddress: string; // Contract address
    attachedValue: string;   // nanoTON
  };
  trackingMeta: {
    sourceProxy: string;
    targetProxy: string;
    targetConfiguration: string;
  };
}

async function sendTransfer(
  tvmClient: ProviderRpcClient,
  senderAddress: string,
  payloadResponse: TransferPayloadResponse
): Promise<string> {
  const { abiMeta } = payloadResponse;

  console.log(`Sending ${abiMeta.abiMethod} transaction...`);
  console.log('Contract address:', abiMeta.executionAddress);
  console.log('Gas:', abiMeta.attachedValue, 'nanoTON');

  // The API returns the contract ABI, method name, and parameters —
  // use them directly for calling via TVM SDK.
  const contract = new tvmClient.Contract(
    JSON.parse(abiMeta.abi),
    new Address(abiMeta.executionAddress)
  );

  const params = JSON.parse(abiMeta.params);
  const { transaction } = await contract.methods[abiMeta.abiMethod](params).send({
    from: new Address(senderAddress),
    amount: abiMeta.attachedValue,
    bounce: true,
  });

  console.log('Transaction sent, hash:', transaction.id.hash);

  return transaction.id.hash;
}

Usage Example

typescript
import TonConnectUI from '@tonconnect/ui';

// TonConnect manifest — a JSON file describing your dApp (name, icon, URL).
// Hosted on your domain. Format: { "url": "...", "name": "...", "iconUrl": "..." }
const tonConnectUI = new TonConnectUI({
  manifestUrl: 'https://your-app.com/tonconnect-manifest.json',
});

// payloadResponse obtained in Step 1
const messageHash = await sendTransfer(tonConnectUI, payloadResponse);
console.log('Message hash obtained:', messageHash);
typescript
import { EverscaleStandaloneClient } from 'everscale-standalone-client';
import { ProviderRpcClient } from 'everscale-inpage-provider';

const tvmClient = new ProviderRpcClient({
  fallback: () => EverscaleStandaloneClient.create({
    connection: { type: 'jrpc', data: { endpoint: 'https://jrpc-ton.broxus.com' } },
  }),
});
await tvmClient.ensureInitialized();

// payloadResponse obtained in Step 1
const txHash = await sendTransfer(
  tvmClient,
  '0:3333...cccc',  // Sender's TVM wallet address
  payloadResponse
);
console.log('Transaction hash obtained:', txHash);

After a Successful Transaction

  1. The wallet transaction triggers a chain of internal calls in the blockchain
  2. In this chain, the Proxy contract (sourceProxy) emits a TvmTvmNative or TvmTvmAlien event
  3. Transfer ID = transaction hash of the transaction in the chain where the event is emitted (not the hash of the original wallet transaction)

Step 3: Getting the Transfer ID

Why You Need the Transfer ID

Transfer ID is needed for:

  • Tracking transfer status via Bridge Aggregator API
  • Getting proof for non-credit mode
  • Diagnosing transfer issues

Transfer ID = transaction hash of the transaction with the TvmTvmNative or TvmTvmAlien event on the Proxy contract.

Transaction on Proxy ← Transaction Hash = TRANSFER ID
    └── Out Messages
        └── External Out Message (TvmTvmNative/TvmTvmAlien event)

How to Get the Transfer ID

Important

result.boc from TON Connect is the BOC of the signed message from the wallet, not the Transfer ID. The Transfer ID only appears after the transaction is processed in the blockchain and the Proxy emits the event.

typescript
// npm install @ton/ton @ton/core

import { Address } from '@ton/ton';

// === CONFIGURATION ===
const TONAPI_BASE = 'https://tetra.tonapi.io/v2'; // Blockchain Explorer API
// const TONAPI_BASE = 'https://testnet.tonapi.io/v2';  // Testnet

/**
 * Gets the transaction trace via TonAPI
 */
async function getTransactionTrace(txHash: string): Promise<any> {
  const response = await fetch(
    `${TONAPI_BASE}/traces/${txHash}`,
    {
      headers: { 'Accept': 'application/json' }
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to get trace: ${response.status}`);
  }

  return response.json();
}

/**
 * Recursively finds the transaction on the Proxy contract with TvmTvmNative/TvmTvmAlien event
 */
function findProxyTransaction(
  node: any,
  proxyAddress: string
): string | null {
  const normalizedProxy = Address.parse(proxyAddress).toRawString().toLowerCase();
  const tx = node.transaction;

  if (tx?.account?.address) {
    const txAddress = tx.account.address.toLowerCase();
    const isProxy = txAddress === normalizedProxy;

    // Check for External Out Message (event)
    const hasEvent = tx.out_msgs?.some(
      (m: any) => !m.destination || m.destination?.address === ''
    );

    if (isProxy && hasEvent) {
      return tx.hash;
    }
  }

  for (const child of node.children || []) {
    const result = findProxyTransaction(child, proxyAddress);
    if (result) return result;
  }

  return null;
}

// === USAGE ===

// messageHash obtained in Step 2 from Cell.fromBase64(result.boc).hash()
const trace = await getTransactionTrace(messageHash);
const transferId = findProxyTransaction(trace, payloadResponse.trackingMeta.sourceProxy);

console.log('Transfer ID:', transferId);
typescript
// When using TVM SDK, the transaction hash is available directly
// from the result of contract.methods[...].send()

// txHash obtained in Step 2 from transaction.id.hash
// To get the Transfer ID, we need the trace of the same transaction

import { Address } from '@ton/ton';

const TONAPI_BASE = 'https://tetra.tonapi.io/v2';

async function getTransactionTrace(txHash: string): Promise<any> {
  const response = await fetch(
    `${TONAPI_BASE}/traces/${txHash}`,
    { headers: { 'Accept': 'application/json' } }
  );
  if (!response.ok) {
    throw new Error(`Failed to get trace: ${response.status}`);
  }
  return response.json();
}

function findProxyTransaction(
  node: any,
  proxyAddress: string
): string | null {
  const normalizedProxy = Address.parse(proxyAddress).toRawString().toLowerCase();
  const tx = node.transaction;

  if (tx?.account?.address) {
    const txAddress = tx.account.address.toLowerCase();
    const isProxy = txAddress === normalizedProxy;
    const hasEvent = tx.out_msgs?.some(
      (m: any) => !m.destination || m.destination?.address === ''
    );
    if (isProxy && hasEvent) {
      return tx.hash;
    }
  }

  for (const child of node.children || []) {
    const result = findProxyTransaction(child, proxyAddress);
    if (result) return result;
  }
  return null;
}

// txHash obtained in Step 2
const trace = await getTransactionTrace(txHash);
const transferId = findProxyTransaction(trace, payloadResponse.trackingMeta.sourceProxy);

console.log('Transfer ID:', transferId);

Step 4: Requesting Proof via Bridge Aggregator API

Goal: Get data to complete the transfer (if useCredit=false)

Conditions

useCreditAction
trueCredit Backend automatically deploys Event. Skip steps 4 and 5.
falseYou need to manually get proof and deploy the Event contract in the destination network.

API Endpoint

POST {BASE_URL}/transfers/status
Content-Type: application/json

Request Example

bash
curl -X POST '{BASE_URL}/transfers/status' \
  -H 'Content-Type: application/json' \
  -d '{
    "tvmTvm": {
      "outgoingTransactionHash": "abc123def456...",
      "dappChainId": -239,
      "timestampCreatedFrom": null
    }
  }'

Request Parameters

ParameterTypeDescription
tvmTvm.outgoingTransactionHashstringTransfer ID obtained in Step 3
tvmTvm.dappChainIdnumberSource network chain ID
tvmTvm.timestampCreatedFromnumber | nullTime filter (optional)

Response Example with Proof

json
{
  "transfer": {
    "tvmTvm": {
      "transferStatus": "Pending",
      "timestampCreatedAt": 1767972717,
      "outgoing": {
        "tokenType": "Native",
        "chainId": -239,
        "userAddress": "0:3333...cccc",
        "tokenAddress": "0:1111...aaaa",
        "proxyAddress": "0:6666...ffff",
        "volumeExec": "0.1000",
        "messageHash": "abc123def456...",
        "transactionHash": "def789abc012..."
      },
      "incoming": {
        "tokenType": null,
        "chainId": 2000,
        "userAddress": "0:2222...bbbb"
      }
    }
  },
  "notInstantTransfer": null,
  "proofPayload": {
    "txBlockProof": "te6ccgECCg...",
    "txProof": "te6ccgEBBw...",
    "messageHash": "abc123def456...",
    "outMessageIndex": 0,
    "event": {
      "tokenType": "Native",
      "chainId": 2000,
      "token": "0:1111...aaaa",
      "amount": "99000",
      "recipient": "0:2222...bbbb",
      "value": "150000000",
      "expectedGas": "0",
      "remainingGasTo": "0:3333...cccc",
      "sender": "0:3333...cccc",
      "payload": "te6ccgEBAQEAAgAAAA==",
      "nativeProxyWallet": "0:9999...2222",
      "name": "Tether USD",
      "symbol": "USD₮",
      "decimals": 6
    },
    "abiTx": {
      "tx": "te6ccgEBBwEA6AABiw/X8c8...",
      "executionAddress": "0:8888...1111",
      "attachedValue": "2000000000",
      "abiMethod": "deployEvent",
      "abi": "...",
      "params": "{\"_eventVoteData\":{...}}"
    }
  }
}

Response Fields

FieldDescription
transfer.tvmTvm.transferStatusTransfer status (Pending, Completed, Failed)
proofPayloadData for deploying the Event contract. null if proof is not yet ready
proofPayload.txBlockProofMerkle proof of the block with the transaction
proofPayload.txProofMerkle proof of the transaction itself
proofPayload.eventEvent data (token, amount, recipient)
proofPayload.abiTxReady transaction for deploying the Event contract (Step 5)

Step 5: Deploying the Event Contract (non-credit)

Goal: Deploy the Event contract in the destination network to complete the transfer

Non-credit only

This step is needed only when useCredit: false. When useCredit: true, the Credit Backend automatically deploys the Event contract.

Transaction Data

FieldDescriptionUsage
proofPayload.abiTx.executionAddressEventConfiguration contract addressTransaction recipient
proofPayload.abiTx.txReady payload (BOC base64)Transaction body
proofPayload.abiTx.attachedValueRequired gas (in nano-units)Transaction amount
proofPayload.abiTx.abiContract ABI (JSON string)For TVM SDK
proofPayload.abiTx.abiMethodContract method (deployEvent)Method name
proofPayload.abiTx.paramsMethod parameters (JSON string)For TVM SDK

Sending the deployEvent Transaction

typescript
import TonConnectUI from '@tonconnect/ui';

interface ProofPayloadResponse {
  abiTx: {
    tx: string;              // BOC base64
    executionAddress: string; // EventConfiguration address
    attachedValue: string;   // nanoTON
    abiMethod: string;
    abi: string;
    params: string;
  } | null;
}

async function deployEventContract(
  tonConnectUI: TonConnectUI,
  proofPayload: ProofPayloadResponse
): Promise<string> {
  const { abiTx } = proofPayload;

  if (!abiTx) {
    throw new Error('abiTx not available in proofPayload');
  }

  console.log('Deploying Event contract...');
  console.log('EventConfiguration:', abiTx.executionAddress);
  console.log('Gas:', abiTx.attachedValue, 'nanoTON');

  // Build transaction using ready data from API
  const transaction = {
    validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes
    messages: [
      {
        address: abiTx.executionAddress,  // EventConfiguration address
        amount: abiTx.attachedValue,      // Gas from API
        payload: abiTx.tx,                // BOC base64 from API
      },
    ],
  };

  // Send transaction via TonConnect
  const result = await tonConnectUI.sendTransaction(transaction);

  console.log('deployEvent transaction sent:', result.boc);

  return result.boc;
}
typescript
import { Address, ProviderRpcClient } from 'everscale-inpage-provider';

interface ProofPayloadResponse {
  abiTx: {
    tx: string;              // BOC base64
    executionAddress: string; // EventConfiguration address
    attachedValue: string;   // nanoTON
    abiMethod: string;       // deployEvent
    abi: string;             // Contract ABI (JSON string)
    params: string;          // Method parameters (JSON string)
  } | null;
}

async function deployEventContract(
  tvmClient: ProviderRpcClient,
  senderAddress: string,
  proofPayload: ProofPayloadResponse
): Promise<string> {
  const { abiTx } = proofPayload;

  if (!abiTx) {
    throw new Error('abiTx not available in proofPayload');
  }

  console.log('Deploying Event contract...');
  console.log('EventConfiguration:', abiTx.executionAddress);
  console.log('Gas:', abiTx.attachedValue, 'nanoTON');

  // The API returns the ABI, method, and parameters for deploying the Event —
  // use them directly via TVM SDK.
  const contract = new tvmClient.Contract(
    JSON.parse(abiTx.abi),
    new Address(abiTx.executionAddress)
  );

  const params = JSON.parse(abiTx.params);
  const { transaction } = await contract.methods[abiTx.abiMethod](params).send({
    from: new Address(senderAddress),
    amount: abiTx.attachedValue,
    bounce: true,
  });

  console.log('deployEvent transaction sent, hash:', transaction.id.hash);

  return transaction.id.hash;
}

What Happens After deployEvent

  1. EventConfiguration deploys a new Event contract with proof data
  2. The Event contract verifies the transaction via TransactionCheckerLiteClient
  3. After successful verification, the Event contract calls the Proxy in the destination network
  4. The Proxy mints (for Alien) or unlocks (for Native) tokens to the recipient

Full Example

End-to-end example of a non-credit TVM→TVM transfer:

typescript
import TonConnectUI from '@tonconnect/ui';
import { Cell } from '@ton/core';
import { Address } from '@ton/ton';

const BASE_URL = 'https://tetra-history-api.chainconnect.com/v2';
const TONAPI_BASE = 'https://tetra.tonapi.io/v2';

const tonConnectUI = new TonConnectUI({
  manifestUrl: 'https://your-app.com/tonconnect-manifest.json',
});

// --- Step 1: Build Payload ---
const payloadResponse = await fetch(`${BASE_URL}/payload/build`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    fromChainId: -239,
    toChainId: 2000,
    tokenAddress: '0:1111...aaaa',
    recipientAddress: '0:2222...bbbb',
    amount: '1000000',
    senderAddress: '0:3333...cccc',
    useCredit: false,
    remainingGasTo: '0:3333...cccc',
  }),
}).then(r => r.json());

console.log('Transfer kind:', payloadResponse.transferKind);
console.log('Amount after fee:', payloadResponse.tokenAmount);

// --- Step 2: Send Transaction ---
const txResult = await tonConnectUI.sendTransaction({
  validUntil: Math.floor(Date.now() / 1000) + 600,
  messages: [{
    address: payloadResponse.abiMeta.executionAddress,
    amount: payloadResponse.abiMeta.attachedValue,
    payload: payloadResponse.abiMeta.tx,
  }],
});

const messageHash = Cell.fromBase64(txResult.boc).hash().toString('hex');
console.log('Message hash:', messageHash);

// --- Step 3: Get Transfer ID ---
// Wait for transaction confirmation (delay for blockchain processing)
await new Promise(resolve => setTimeout(resolve, 15000));

const trace = await fetch(`${TONAPI_BASE}/traces/${messageHash}`, {
  headers: { 'Accept': 'application/json' },
}).then(r => r.json());

function findProxyTx(node: any, proxy: string): string | null {
  const normalized = Address.parse(proxy).toRawString().toLowerCase();
  const tx = node.transaction;
  if (tx?.account?.address?.toLowerCase() === normalized) {
    const hasEvent = tx.out_msgs?.some(
      (m: any) => !m.destination || m.destination?.address === ''
    );
    if (hasEvent) return tx.hash;
  }
  for (const child of node.children || []) {
    const r = findProxyTx(child, proxy);
    if (r) return r;
  }
  return null;
}

const transferId = findProxyTx(trace, payloadResponse.trackingMeta.sourceProxy);
console.log('Transfer ID:', transferId);

// --- Step 4: Request Proof ---
const statusResponse = await fetch(`${BASE_URL}/transfers/status`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    tvmTvm: {
      outgoingTransactionHash: transferId,
      dappChainId: -239,
    },
  }),
}).then(r => r.json());

console.log('Status:', statusResponse.transfer?.tvmTvm?.transferStatus);

// --- Step 5: Deploy Event Contract ---
if (statusResponse.proofPayload?.abiTx) {
  const deployResult = await tonConnectUI.sendTransaction({
    validUntil: Math.floor(Date.now() / 1000) + 600,
    messages: [{
      address: statusResponse.proofPayload.abiTx.executionAddress,
      amount: statusResponse.proofPayload.abiTx.attachedValue,
      payload: statusResponse.proofPayload.abiTx.tx,
    }],
  });
  console.log('Event deployed:', deployResult.boc);
}
typescript
import { EverscaleStandaloneClient } from 'everscale-standalone-client';
import { Address, ProviderRpcClient } from 'everscale-inpage-provider';

const BASE_URL = 'https://tetra-history-api.chainconnect.com/v2';
const TONAPI_BASE = 'https://tetra.tonapi.io/v2';

const tvmClient = new ProviderRpcClient({
  fallback: () => EverscaleStandaloneClient.create({
    connection: { type: 'jrpc', data: { endpoint: 'https://jrpc-ton.broxus.com' } },
  }),
});
await tvmClient.ensureInitialized();

const senderAddress = '0:3333...cccc';

// --- Step 1: Build Payload ---
const payloadResponse = await fetch(`${BASE_URL}/payload/build`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    fromChainId: -239,
    toChainId: 2000,
    tokenAddress: '0:1111...aaaa',
    recipientAddress: '0:2222...bbbb',
    amount: '1000000',
    senderAddress,
    useCredit: false,
    remainingGasTo: senderAddress,
  }),
}).then(r => r.json());

console.log('Transfer kind:', payloadResponse.transferKind);
console.log('Amount after fee:', payloadResponse.tokenAmount);

// --- Step 2: Send Transaction ---
const contract = new tvmClient.Contract(
  JSON.parse(payloadResponse.abiMeta.abi),
  new Address(payloadResponse.abiMeta.executionAddress)
);

const params = JSON.parse(payloadResponse.abiMeta.params);
const { transaction } = await contract.methods[payloadResponse.abiMeta.abiMethod](params).send({
  from: new Address(senderAddress),
  amount: payloadResponse.abiMeta.attachedValue,
  bounce: true,
});

const txHash = transaction.id.hash;
console.log('Transaction hash:', txHash);

// --- Step 3: Get Transfer ID ---
// Wait for transaction confirmation
await new Promise(resolve => setTimeout(resolve, 15000));

const trace = await fetch(`${TONAPI_BASE}/traces/${txHash}`, {
  headers: { 'Accept': 'application/json' },
}).then(r => r.json());

function findProxyTx(node: any, proxy: string): string | null {
  const normalized = Address.parse(proxy).toRawString().toLowerCase();
  const tx = node.transaction;
  if (tx?.account?.address?.toLowerCase() === normalized) {
    const hasEvent = tx.out_msgs?.some(
      (m: any) => !m.destination || m.destination?.address === ''
    );
    if (hasEvent) return tx.hash;
  }
  for (const child of node.children || []) {
    const r = findProxyTx(child, proxy);
    if (r) return r;
  }
  return null;
}

const transferId = findProxyTx(trace, payloadResponse.trackingMeta.sourceProxy);
console.log('Transfer ID:', transferId);

// --- Step 4: Request Proof ---
const statusResponse = await fetch(`${BASE_URL}/transfers/status`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    tvmTvm: {
      outgoingTransactionHash: transferId,
      dappChainId: -239,
    },
  }),
}).then(r => r.json());

console.log('Status:', statusResponse.transfer?.tvmTvm?.transferStatus);

// --- Step 5: Deploy Event Contract ---
if (statusResponse.proofPayload?.abiTx) {
  const { abiTx } = statusResponse.proofPayload;

  const eventConfig = new tvmClient.Contract(
    JSON.parse(abiTx.abi),
    new Address(abiTx.executionAddress)
  );

  const eventParams = JSON.parse(abiTx.params);
  const { transaction: deployTx } = await eventConfig.methods[abiTx.abiMethod](eventParams).send({
    from: new Address(senderAddress),
    amount: abiTx.attachedValue,
    bounce: true,
  });

  console.log('Event deployed, hash:', deployTx.id.hash);
}

Error Reference

Proxy Contract Errors

CodeConstantDescription
2701NOT_EVM_CONFIGSender is not an EVM EventConfiguration
2702PROXY_PAUSEDProxy contract is paused
2703PROXY_TOKEN_ROOT_IS_EMPTYToken Root is not set
2704WRONG_TOKENS_AMOUNT_IN_PAYLOADInvalid token amount in payload
2705WRONG_OWNER_IN_PAYLOADInvalid owner in payload
2708WRONG_TOKEN_ROOTInvalid Token Root
2710NOT_TVM_CONFIGSender is not a TVM EventConfiguration
2713LOW_MSG_VALUEInsufficient gas (attached value too low)

Event Configuration Errors

CodeConstantDescription
2209SENDER_NOT_BRIDGESender is not the Bridge contract
2210EVENT_BLOCK_NUMBER_LESS_THAN_STARTEvent block number is less than the start
2211EVENT_TIMESTAMP_LESS_THAN_STARTEvent timestamp is earlier than the start
2212SENDER_NOT_EVENT_CONTRACTSender is not an Event contract
2213TOO_LOW_DEPLOY_VALUEInsufficient gas for Event deployment
2220SENDER_IS_NOT_EVENT_EMITTERSender is not the event emitter
2221WRONG_DISPATCH_CHAIN_IDInvalid source chain ID
2222WRONG_MESSAGE_HASHInvalid message hash
2223WRONG_DESTINATION_CHAIN_IDInvalid destination chain ID

Event Contract Errors

CodeConstantDescription
2312EVENT_NOT_PENDINGEvent is not in Pending status
2316EVENT_NOT_CONFIRMEDEvent is not confirmed
2317TOO_LOW_MSG_VALUEInsufficient gas for processing
2321EVENT_NOT_INITIALIZINGEvent is not in initializing status
2329SENDER_NOT_TX_CHECKERSender is not TransactionChecker
2330WRONG_BASE_NATIVE_PROXY_WALLETInvalid Native Proxy Wallet address

Bridge Errors

CodeConstantDescription
2102BRIDGE_NOT_ACTIVEBridge is not active
2103EVENT_CONFIGURATION_NOT_ACTIVEEventConfiguration is not active
2108BRIDGE_PAUSEDBridge is paused
2112ZERO_ADDRESSZero address provided

ChainConnect Bridge Documentation