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/jsonTest Environment
For testing, use https://history-api-test.chainconnect.com/v2/payload/build
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
fromChainId | number | ✅ | Chain ID of the source network |
toChainId | number | ✅ | Chain ID of the destination network |
tokenAddress | string | ✅ | Token address in the source network (format 0:...) |
recipientAddress | string | ✅ | Recipient address in the destination network |
amount | string | ✅ | Amount in nano-units (integer string, e.g. "1000000" for 1 USDT with 6 decimals) |
senderAddress | string | ✅ | Sender address in the source network |
useCredit | boolean | ❌ | true = Credit Backend pays gas in the destination network. Default: false |
remainingGasTo | string | ❌ | Where to return remaining gas. Ignored if useCredit=true |
tokenBalance.nativeTokenAmount | string | ⚠️ | Native currency (gas token) balance. Required when sending the gas token |
tokenBalance.wrappedNativeTokenAmount | string | ⚠️ | Wrapped gas token balance (e.g., wTON). Required when sending the gas token |
Request Example
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
{
"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
| Field | Description | Usage |
|---|---|---|
abiMeta.executionAddress | Address for sending the transaction | Step 2 |
abiMeta.tx | Transaction payload (BOC base64) | Step 2 |
abiMeta.attachedValue | Amount to attach to TX (nanoTON) | Step 2 |
trackingMeta.sourceProxy | Proxy contract address | For transfer tracking (Step 3) |
Step 2: Sending the Transaction to Blockchain
Goal: Execute the transaction and get the Transaction Hash
Transaction Parameters
| Parameter | Value | Source |
|---|---|---|
recipient | abiMeta.executionAddress | From Step 1 response |
amount | abiMeta.attachedValue | From Step 1 response |
payload | abiMeta.tx | From Step 1 response |
bounce | true | Always |
TypeScript Example (TonConnect UI)
Documentation
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
- The wallet transaction triggers a chain of internal calls in the blockchain
- In this chain, the Proxy contract (
sourceProxy) emits aTvmTvmNativeorTvmTvmAlienevent - 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:
- Wait for the transaction to be confirmed in the blockchain
- Get the trace of the internal transaction chain
- Find the transaction on the Proxy contract with an External Out Message (event)
- The hash of this transaction = Transfer ID
Example (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:
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
useCredit | Action |
|---|---|
true | Credit Backend automatically deploys event. Skip this step. |
false | You 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/jsonRequest Example
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
{
"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)
- Get
proofPayloadfrom the API response - Send a transaction to
EventConfiguration.deployEvent()in the destination network - The Event contract verifies the transaction via
TransactionChecker→LiteClient - 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.
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).