Sending a Transfer
Overview
A TVM↔TVM transfer allows sending tokens between TVM networks (e.g., TON ↔ Tetra L2). The process includes:
- Build Payload — get transaction data via API
- Send Transaction — send a transaction in TVM (transfer via Proxy)
- Get Transfer ID — find the hash of the transaction with the event on the Proxy contract
- Request Proof (non-credit) — get transaction proof via API
- 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
TvmTvmNativeorTvmTvmAlienevent - The API generates a Merkle proof of the transaction (
txBlockProofandtxProoffields) - The Event contract in the destination network verifies the proof via
TransactionChecker→LiteClient - 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 Event | Gas Credit Backend automatically | User manually (Steps 4–5) |
| Proof needed | No (Backend handles it) | Yes (via API) |
| Number of transactions | 1 (send) | 2 (send + deployEvent) |
| Gas in destination | Paid by Gas Credit Backend | Paid by user |
| Speed | Faster | Depends 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
| Type | Description |
|---|---|
| Native | A 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. |
| Alien | A 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
| Environment | Base URL |
|---|---|
| Production | https://tetra-history-api.chainconnect.com/v2 |
| Testnet | https://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/jsonRequest Parameters
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)
};
}| Parameter | Type | Required | Description |
|---|---|---|---|
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, e.g. "1000000" for 1 USDT with 6 decimals) |
senderAddress | string | ✅ | Sender address in source network |
useCredit | boolean | ❌ | true = Credit Backend pays gas in destination network. Default: true |
remainingGasTo | string | ✅ | Where to return remaining gas. Required for TVM→TVM |
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 '{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
{
"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
| Field | Description | Usage |
|---|---|---|
transferKind | Transfer type (TvmToTvm) | Direction confirmation |
tokenAmount | Final amount after fee deduction | Amount the recipient will receive |
feesMeta.amount | Bridge fee amount | Fee information |
abiMeta.executionAddress | Contract address for the transaction | TVM transaction recipient (Step 2) |
abiMeta.tx | Full call BOC (base64) | Ready body for TonConnect (Step 2) |
abiMeta.abi | Contract ABI (JSON string) | For calling via TVM SDK (Step 2) |
abiMeta.abiMethod | Contract method (transfer) | Method name for the call (Step 2) |
abiMeta.params | Method parameters (JSON string) | Parameters for TVM SDK call (Step 2) |
abiMeta.attachedValue | Amount of nanoTON to send | TVM transaction gas (Step 2) |
trackingMeta.sourceProxy | Proxy contract address | For getting Transfer ID (Step 3) |
trackingMeta.targetConfiguration | EventConfiguration address in destination | For 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
| Parameter | Description | Usage |
|---|---|---|
abiMeta.tx | Full transfer call BOC | For TonConnect — ready message body |
abiMeta.abi | Contract ABI (JSON) | For TVM SDK — creating Contract |
abiMeta.abiMethod | "transfer" | For TVM SDK — method name |
abiMeta.params | Method parameters (JSON) | For TVM SDK — call arguments |
abiMeta.executionAddress | Contract address | Transaction recipient |
abiMeta.attachedValue | Gas in nanoTON | Transaction amount |
Sending the Transaction
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;
}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
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);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
- 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.
// 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);// 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
useCredit | Action |
|---|---|
true | Credit Backend automatically deploys Event. Skip steps 4 and 5. |
false | You need to manually get proof and deploy the Event contract in the destination network. |
API Endpoint
POST {BASE_URL}/transfers/status
Content-Type: application/jsonRequest Example
curl -X POST '{BASE_URL}/transfers/status' \
-H 'Content-Type: application/json' \
-d '{
"tvmTvm": {
"outgoingTransactionHash": "abc123def456...",
"dappChainId": -239,
"timestampCreatedFrom": null
}
}'Request Parameters
| Parameter | Type | Description |
|---|---|---|
tvmTvm.outgoingTransactionHash | string | Transfer ID obtained in Step 3 |
tvmTvm.dappChainId | number | Source network chain ID |
tvmTvm.timestampCreatedFrom | number | null | Time filter (optional) |
Response Example with Proof
{
"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
| Field | Description |
|---|---|
transfer.tvmTvm.transferStatus | Transfer status (Pending, Completed, Failed) |
proofPayload | Data for deploying the Event contract. null if proof is not yet ready |
proofPayload.txBlockProof | Merkle proof of the block with the transaction |
proofPayload.txProof | Merkle proof of the transaction itself |
proofPayload.event | Event data (token, amount, recipient) |
proofPayload.abiTx | Ready 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
| Field | Description | Usage |
|---|---|---|
proofPayload.abiTx.executionAddress | EventConfiguration contract address | Transaction recipient |
proofPayload.abiTx.tx | Ready payload (BOC base64) | Transaction body |
proofPayload.abiTx.attachedValue | Required gas (in nano-units) | Transaction amount |
proofPayload.abiTx.abi | Contract ABI (JSON string) | For TVM SDK |
proofPayload.abiTx.abiMethod | Contract method (deployEvent) | Method name |
proofPayload.abiTx.params | Method parameters (JSON string) | For TVM SDK |
Sending the deployEvent Transaction
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;
}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
- EventConfiguration deploys a new Event contract with proof data
- The Event contract verifies the transaction via
TransactionChecker→LiteClient - After successful verification, the Event contract calls the Proxy in the destination network
- 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:
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);
}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
| Code | Constant | Description |
|---|---|---|
| 2701 | NOT_EVM_CONFIG | Sender is not an EVM EventConfiguration |
| 2702 | PROXY_PAUSED | Proxy contract is paused |
| 2703 | PROXY_TOKEN_ROOT_IS_EMPTY | Token Root is not set |
| 2704 | WRONG_TOKENS_AMOUNT_IN_PAYLOAD | Invalid token amount in payload |
| 2705 | WRONG_OWNER_IN_PAYLOAD | Invalid owner in payload |
| 2708 | WRONG_TOKEN_ROOT | Invalid Token Root |
| 2710 | NOT_TVM_CONFIG | Sender is not a TVM EventConfiguration |
| 2713 | LOW_MSG_VALUE | Insufficient gas (attached value too low) |
Event Configuration Errors
| Code | Constant | Description |
|---|---|---|
| 2209 | SENDER_NOT_BRIDGE | Sender is not the Bridge contract |
| 2210 | EVENT_BLOCK_NUMBER_LESS_THAN_START | Event block number is less than the start |
| 2211 | EVENT_TIMESTAMP_LESS_THAN_START | Event timestamp is earlier than the start |
| 2212 | SENDER_NOT_EVENT_CONTRACT | Sender is not an Event contract |
| 2213 | TOO_LOW_DEPLOY_VALUE | Insufficient gas for Event deployment |
| 2220 | SENDER_IS_NOT_EVENT_EMITTER | Sender is not the event emitter |
| 2221 | WRONG_DISPATCH_CHAIN_ID | Invalid source chain ID |
| 2222 | WRONG_MESSAGE_HASH | Invalid message hash |
| 2223 | WRONG_DESTINATION_CHAIN_ID | Invalid destination chain ID |
Event Contract Errors
| Code | Constant | Description |
|---|---|---|
| 2312 | EVENT_NOT_PENDING | Event is not in Pending status |
| 2316 | EVENT_NOT_CONFIRMED | Event is not confirmed |
| 2317 | TOO_LOW_MSG_VALUE | Insufficient gas for processing |
| 2321 | EVENT_NOT_INITIALIZING | Event is not in initializing status |
| 2329 | SENDER_NOT_TX_CHECKER | Sender is not TransactionChecker |
| 2330 | WRONG_BASE_NATIVE_PROXY_WALLET | Invalid Native Proxy Wallet address |
Bridge Errors
| Code | Constant | Description |
|---|---|---|
| 2102 | BRIDGE_NOT_ACTIVE | Bridge is not active |
| 2103 | EVENT_CONFIGURATION_NOT_ACTIVE | EventConfiguration is not active |
| 2108 | BRIDGE_PAUSED | Bridge is paused |
| 2112 | ZERO_ADDRESS | Zero address provided |