Skip to content

Sending a Transfer

Step 1: Preparing the Transfer

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

API Endpoint

POST https://tetra-history-api.chainconnect.com/v2/payload/build
Content-Type: application/json

Test Environment

For testing, use https://history-api-test.chainconnect.com/v2/payload/build

Request Parameters

ParameterTypeRequiredDescription
fromChainIdnumberChain ID of the source network
toChainIdnumberChain ID of the destination network
tokenAddressstringToken address in the source network (format 0:...)
recipientAddressstringRecipient address in the destination network
amountstringAmount in nano-units (integer string, e.g. "1000000" for 1 USDT with 6 decimals)
senderAddressstringSender address in the source network
useCreditbooleantrue = Credit Backend pays gas in the destination network. Default: false
remainingGasTostringWhere to return remaining gas. Ignored if useCredit=true
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 'https://tetra-history-api.chainconnect.com/v2/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": true,
    "remainingGasTo": "0:3333...cccc"
  }'

Response Example

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

What You'll Need Next

FieldDescriptionUsage
abiMeta.executionAddressAddress for sending the transactionStep 2
abiMeta.txTransaction payload (BOC base64)Step 2
abiMeta.attachedValueAmount to attach to TX (nanoTON)Step 2
trackingMeta.sourceProxyProxy contract addressFor transfer tracking (Step 3)

Step 2: Sending the Transaction to Blockchain

Goal: Execute the transaction and get the Transaction Hash

Transaction Parameters

ParameterValueSource
recipientabiMeta.executionAddressFrom Step 1 response
amountabiMeta.attachedValueFrom Step 1 response
payloadabiMeta.txFrom Step 1 response
bouncetrueAlways

TypeScript Example (TonConnect UI)

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

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

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

  // Build the transaction
  const transaction = {
    validUntil: Math.floor(Date.now() / 1000) + 300, // 5 minutes
    messages: [
      {
        address: abiMeta.executionAddress,  // Contract address
        amount: abiMeta.attachedValue,      // Gas in nanoTON
        payload: abiMeta.tx,                // BOC base64 from API
      },
    ],
  };

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

  console.log('Transaction BOC:', result.boc);

  return result.boc;
}

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.

After sending the transaction, you need to:

  1. Wait for the transaction to be confirmed in the blockchain
  2. Get the trace of the internal transaction chain
  3. Find the transaction on the Proxy contract with an External Out Message (event)
  4. The hash of this transaction = Transfer ID

Example (TypeScript)

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

import { Cell, beginCell, Address, TonClient, Transaction } from '@ton/ton';
import { sha256 } from '@ton/crypto';

// === CONFIGURATION ===
const TONCENTER_API = 'https://toncenter.com/api/v2';  // Mainnet
// const TONCENTER_API = 'https://testnet.toncenter.com/api/v2';  // Testnet

/**
 * Parses BOC from TonConnect and returns the external message hash
 */
function getExternalMessageHash(bocBase64: string): string {
  const cell = Cell.fromBase64(bocBase64);
  return cell.hash().toString('hex');
}

/**
 * Waits for the transaction to appear in the blockchain
 */
async function waitForTransaction(
  walletAddress: string,
  messageHash: string,
  maxAttempts = 30,
  delayMs = 3000
): Promise<Transaction | null> {
  const address = Address.parse(walletAddress);

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetch(
      `${TONCENTER_API}/getTransactions?` +
      `address=${address.toString()}&limit=10&archival=true`
    );
    const data = await response.json();

    if (!data.ok || !data.result) {
      await new Promise(r => setTimeout(r, delayMs));
      continue;
    }

    // Look for transaction with our incoming message
    for (const tx of data.result) {
      if (tx.in_msg?.hash === messageHash) {
        console.log(`Transaction found at attempt ${attempt + 1}`);
        return tx;
      }
    }

    console.log(`Attempt ${attempt + 1}: waiting for transaction...`);
    await new Promise(r => setTimeout(r, delayMs));
  }

  return null;
}

/**
 * Gets the transaction trace (all internal transactions in the chain)
 */
async function getTransactionTrace(
  txHash: string,
  txLt: string
): Promise<any[]> {
  // Use TonAPI to get trace (more convenient API for this)
  const response = await fetch(
    `https://tonapi.io/v2/traces/${txHash}`,
    {
      headers: {
        'Accept': 'application/json',
      }
    }
  );

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

  const trace = await response.json();
  return flattenTrace(trace);
}

/**
 * Recursively extracts all transactions from trace
 */
function flattenTrace(node: any, result: any[] = []): any[] {
  if (node.transaction) {
    result.push(node.transaction);
  }
  if (node.children) {
    for (const child of node.children) {
      flattenTrace(child, result);
    }
  }
  return result;
}

/**
 * Finds the transaction on the Proxy contract with TvmTvmNative/TvmTvmAlien event
 */
function findProxyTransaction(
  transactions: any[],
  proxyAddress: string
): { hash: string; lt: string } | null {
  const normalizedProxy = Address.parse(proxyAddress).toString();

  for (const tx of transactions) {
    const txAccount = tx.account?.address;
    if (!txAccount) continue;

    // Check if transaction is on the Proxy contract
    const txAddress = Address.parseRaw(txAccount).toString();
    if (txAddress !== normalizedProxy) continue;

    // Check for External Out Message (event)
    const outMsgs = tx.out_msgs || [];
    const hasExternalOut = outMsgs.some(
      (msg: any) => msg.destination === null || msg.destination === ''
    );

    if (hasExternalOut) {
      return {
        hash: tx.hash,
        lt: tx.lt,
      };
    }
  }

  return null;
}

/**
 * Main function: gets Transfer ID from BOC
 */
async function getTransferIdFromBoc(
  bocBase64: string,
  walletAddress: string,
  proxyAddress: string
): Promise<string> {
  console.log('=== Step 1: Parsing BOC ===');
  const messageHash = getExternalMessageHash(bocBase64);
  console.log('External message hash:', messageHash);

  console.log('\n=== Step 2: Waiting for transaction ===');
  const walletTx = await waitForTransaction(walletAddress, messageHash);
  if (!walletTx) {
    throw new Error('Transaction not found in blockchain');
  }
  console.log('Wallet transaction hash:', walletTx.transaction_id.hash);
  console.log('Wallet transaction lt:', walletTx.transaction_id.lt);

  console.log('\n=== Step 3: Getting transaction trace ===');
  const trace = await getTransactionTrace(
    walletTx.transaction_id.hash,
    walletTx.transaction_id.lt
  );
  console.log(`Found ${trace.length} transactions in trace`);

  console.log('\n=== Step 4: Finding Proxy transaction ===');
  const proxyTx = findProxyTransaction(trace, proxyAddress);
  if (!proxyTx) {
    throw new Error('Proxy transaction not found in trace');
  }

  console.log('\n=== Result ===');
  console.log('Transfer ID:', proxyTx.hash);

  return proxyTx.hash;
}

// === USAGE EXAMPLE ===

async function example() {
  // After sending transaction via TonConnect:
  // const result = await tonConnectUI.sendTransaction(transaction);
  // const bocBase64 = result.boc;

  const bocBase64 = 'te6cckEBAgEA...';  // BOC from TonConnect
  const walletAddress = '0:3333...cccc';  // Sender wallet address
  const proxyAddress = '0:6666...ffff';   // From payload response (trackingMeta.sourceProxy)

  const transferId = await getTransferIdFromBoc(bocBase64, walletAddress, proxyAddress);
  console.log('Transfer ID:', transferId);
}

// example().catch(console.error);

Alternative: via TonClient (standalone)

If you're using a standalone client without TonConnect, you can get the Transfer ID directly via TonClient:

typescript
import { TonClient, Address } from '@ton/ton';

async function getTransferIdFromWalletTx(
  client: TonClient,
  walletTxHash: string,
  proxyAddress: string
): Promise<string | null> {
  // Get trace via TonAPI
  const response = await fetch(`https://tonapi.io/v2/traces/${walletTxHash}`);
  const trace = await response.json();

  const normalizedProxy = Address.parse(proxyAddress).toRawString().toLowerCase();

  // Recursively search for transaction on Proxy
  function findInTrace(node: any): string | null {
    const tx = node.transaction;
    if (tx?.account?.address) {
      const txAddress = tx.account.address.toLowerCase();
      const isProxy = txAddress.includes(normalizedProxy.slice(2));

      // 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 = findInTrace(child);
      if (result) return result;
    }
    return null;
  }

  return findInTrace(trace);
}

// Usage
const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' });
const transferId = await getTransferIdFromWalletTx(
  client,
  'abc123...',      // Wallet transaction hash
  '0:6666...ffff'   // proxyAddress from trackingMeta.sourceProxy
);

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 this step.
falseYou need to manually get proof and deploy the event contract in the destination network.

API Endpoint

POST https://tetra-history-api.chainconnect.com/v2/transfers/status
Content-Type: application/json

Request Example

bash
curl -X POST 'https://tetra-history-api.chainconnect.com/v2/transfers/status' \
  -H 'Content-Type: application/json' \
  -d '{
    "tvmTvm": {
      "outgoingTransactionHash": "abc123def456...",
      "dappChainId": -239,
      "timestampCreatedFrom": null
    }
  }'

Response Example with Proof

json
{
  "transfer": {
    "tvmTvm": {
      "transferStatus": "Pending",
      "timestampCreatedAt": 1767972717,
      "outgoing": {
        "tokenType": "Native",
        "contractAddress": "0:aaaa...1111",
        "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
    },
    "feeAmount": "1000"
  }
}

What to Do with Proof (for non-credit)

  1. Get proofPayload from the API response
  2. Send a transaction to EventConfiguration.deployEvent() in the destination network
  3. The Event contract verifies the transaction via TransactionCheckerLiteClient
  4. After successful verification, the proxy in the destination network mints/unlocks tokens

Sending Transaction for non-credit

Required Gas

To deploy the Event contract, you need to attach ~3-5 TON (or equivalent in the destination network's native currency). Recommended value: 5 TON for guaranteed execution.

typescript
import { Address, toNano, beginCell } from '@ton/ton';

async function deployEventContract(
  client: TonClient,
  eventConfigurationAddress: string,  // EventConfiguration address in destination network
  proofPayload: ProofPayload,
  senderWallet: any
): Promise<void> {
  const eventConfig = Address.parse(eventConfigurationAddress);

  // Build payload for deployEvent
  // Structure depends on specific contract implementation
  const deployPayload = beginCell()
    .storeRef(Cell.fromBase64(proofPayload.txBlockProof))  // block proof
    .storeRef(Cell.fromBase64(proofPayload.txProof))       // transaction proof
    .storeUint(proofPayload.outMessageIndex, 16)           // message index
    .endCell();

  // Send transaction with sufficient gas
  await senderWallet.sendTransfer({
    to: eventConfig,
    value: toNano('5'),  // 5 TON for gas
    body: deployPayload,
  });

  console.log('deployEvent transaction sent');
}

// Usage
const { proofPayload } = await getTransferStatus(transferId, chainId);

if (proofPayload) {
  await deployEventContract(
    client,
    '0:8888...1111',  // EventConfiguration address (from trackingMeta.targetConfiguration)
    proofPayload,
    wallet
  );
}

EventConfiguration Address

The EventConfiguration address in the destination network can be obtained from the trackingMeta.targetConfiguration field in the /v2/payload/build response (Step 1).

ChainConnect Bridge Documentation