EVM→TVM Transfer
Overview
An EVM→TVM transfer allows you to send tokens from an EVM network to a TVM network. The process includes:
- Deposit to MultiVault — the user sends tokens to the EVM contract (lock for Alien, burn for Native)
- Event indexing — relay nodes and indexers track the deposit
- Event contract deployment — a relay node creates an Event contract in the TVM network
- Relay node consensus — relay nodes call
confirm()(no signatures, only voting) - Callback to Proxy — when enough votes are collected, the Event calls a callback to the Proxy contract
- Final action — Proxy mints/unlocks tokens for the user in the TVM network
Consensus mechanism (EVM→TVM)
For EVM→TVM, consensus is achieved through relay node voting in the TVM network:
- Relay nodes call
confirm(voteReceiver)via TVM external messages (signed with the relay key) - The relay is identified via
msg.pubkey()from the external message signature - Consensus:
confirms >= requiredVotes, whererequiredVotes = keys.length * 2/3 + 1 - After confirmation, the Event contract immediately calls a callback to the Proxy contract
- Proxy performs mint/unlock of tokens in the TVM network
Explicit cryptographic signatures are not passed as parameters — relay authentication happens through TVM's built-in external message signing mechanism.
Token types
| Type | EVM operation | TVM operation | Description |
|---|---|---|---|
| Alien | lock in MultiVault | mint alien | Token from EVM is locked, an alien representation is created in TVM |
| Native | burn in MultiVault | unlock native | Token is returned from EVM, unlocked in TVM |
API Base URL
All API requests in this guide use one of two environments:
| Environment | Base URL |
|---|---|
| Production | https://tetra-history-api.chainconnect.com/v2 |
| Testnet | https://history-api-test.chainconnect.com/v2 |
Endpoints used in the guide:
| Endpoint | Method | Step |
|---|---|---|
/payload/build | POST | Step 1 — build payload |
/transfers/status | POST | Step 3 — check status |
/payload/configurations/all | GET | Step 4.1 — bridge configurations |
/transfers/search | POST | Search transfers |
Step 1: Prepare the transfer (Build Payload)
Goal: Obtain the payload for sending a transaction in the EVM network
API Endpoint
POST {BASE_URL}/payload/build
Content-Type: application/jsonRequest parameters
interface TransferPayloadRequest {
fromChainId: number; // Chain ID of the source EVM network
toChainId: number; // Chain ID of the target TVM network
tokenAddress: string; // Token address in EVM (hex format 0x...)
recipientAddress: string; // Recipient address in TVM (format 0:...)
amount: string; // Amount in nano-units (integer string)
senderAddress: string; // Sender address in EVM (hex format 0x...)
useCredit?: boolean; // true = Credit Backend pays gas. Default: false
remainingGasTo?: string; // Where to return remaining gas in TVM (ignored when useCredit=true)
payload?: string; // Additional payload (base64)
callback?: CallbackRequest; // Callback on completion
evmChainId?: number; // Explicit EVM chain ID
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 | ✅ | Chain ID of the source EVM network |
toChainId | number | ✅ | Chain ID of the target TVM network |
tokenAddress | string | ✅ | ERC-20 token address in EVM (format 0x... lowercase) |
recipientAddress | string | ✅ | Recipient address in TVM (format 0:...) |
amount | string | ✅ | Amount in smallest units (integer as string, e.g. "1000000" for 1 USDT with 6 decimals) |
senderAddress | string | ✅ | Sender address in EVM (format 0x... lowercase) |
useCredit | boolean | ❌ | If true — Gas Credit Backend automatically deploys the Event contract in TVM and pays gas. Default: false |
remainingGasTo | string | ❌ | Address for returning remaining gas (used when useCredit=false) |
callback | object | ❌ | Callback parameters (EVM contract address to call after transfer completion) |
tokenBalance.nativeTokenAmount | string | ⚠️ | Gas token balance (native currency). Required when sending gas token |
tokenBalance.wrappedNativeTokenAmount | string | ⚠️ | Wrapped gas token balance. Required when sending gas token |
Request example
curl -X POST '{BASE_URL}/payload/build' \
-H 'Content-Type: application/json' \
-d '{
"fromChainId": 1,
"toChainId": -239,
"tokenAddress": "0xdAC1...1ec7",
"recipientAddress": "0:1111...1111",
"amount": "1000000",
"senderAddress": "0x742d...Ab12",
"useCredit": true
}'Response example
{
"transferKind": "EvmToTvm",
"tokensMeta": {
"targetToken": {
"tokenType": "Alien",
"isDeployed": true,
"address": "0:2222...2222"
},
"sourceTokenType": "Alien"
},
"tokenAmount": "999000",
"feesMeta": {
"amount": "1000",
"numerator": "1",
"denominator": "1000"
},
"gasEstimateAmount": "50000000000000000",
"abiMeta": {
"tx": "0x...",
"executionAddress": "0x3333...3333",
"attachedValue": "50000000000000000",
"abiMethod": "deposit",
"abi": "{...}",
"params": "{...}"
},
"trackingMeta": {
"sourceProxy": null,
"sourceConfiguration": null,
"sourceMultivault": "0x3333...3333",
"targetProxy": "0:4444...4444",
"targetConfiguration": null,
"targetMultivault": null
},
"payload": null
}Response fields
| Field | Description | Usage |
|---|---|---|
transferKind | Transfer type (EvmToTvm) | Direction confirmation |
tokensMeta.targetToken.tokenType | Token type in the target network (Alien or Native) | Token type information |
tokenAmount | Final amount after fee deduction (in nano-units) | Amount the user will receive |
feesMeta.amount | Bridge fee amount (in nano-units) | Fee information |
feesMeta.numerator / denominator | Fee fraction (numerator/denominator) | Fee percentage calculation |
abiMeta.executionAddress | MultiVault contract address in EVM | EVM transaction recipient (Step 2) |
abiMeta.tx | Transaction calldata (hex) | EVM transaction data (Step 2) |
abiMeta.attachedValue | Amount of ETH/BNB/etc to send (in wei) | Transaction msg.value (Step 2) |
abiMeta.abiMethod | Contract method (deposit or depositByNativeToken) | Method information |
abiMeta.params | Call parameters (JSON) | For building calldata |
trackingMeta.sourceMultivault | MultiVault address in EVM | Informational |
trackingMeta.targetProxy | Proxy contract address in TVM | Informational |
Step 2: Send transaction in EVM
Goal: Execute a token deposit to MultiVault (lock for Alien, burn for Native) and obtain the Transaction Hash
What you need from Step 1
| Response field | Description | Usage |
|---|---|---|
abiMeta.executionAddress | MultiVault contract address | to field in the EVM transaction |
abiMeta.tx | Transaction calldata (hex) | data field in the EVM transaction |
abiMeta.attachedValue | Amount of wei to send | value field in the EVM transaction |
abiMeta.abiMethod | deposit or depositByNativeToken | Determines whether approve is needed (Step 2.1) |
Deposit methods
| Method | When to use | msg.value |
|---|---|---|
deposit() | For ERC-20 tokens (Alien) | Gas only for credit mode |
depositByNativeToken() | For native currency (ETH, BNB, etc.) | Deposit amount + gas |
Step 2.1: Token approve (for ERC-20)
Before depositing an ERC-20 token, you need to allow MultiVault to spend tokens:
import { ethers } from 'ethers';
async function approveToken(
provider: ethers.BrowserProvider,
tokenAddress: string,
spenderAddress: string,
amount: string
): Promise<string> {
const signer = await provider.getSigner();
const erc20Abi = [
'function approve(address spender, uint256 amount) returns (bool)',
'function allowance(address owner, address spender) view returns (uint256)'
];
const token = new ethers.Contract(tokenAddress, erc20Abi, signer);
// Check current allowance
const currentAllowance = await token.allowance(
await signer.getAddress(),
spenderAddress
);
if (currentAllowance >= BigInt(amount)) {
console.log('Allowance is sufficient, approve not required');
return '';
}
// Execute approve
const tx = await token.approve(spenderAddress, amount);
const receipt = await tx.wait();
console.log('Approve confirmed:', receipt.hash);
return receipt.hash;
}Step 2.2: Send deposit
import { ethers } from 'ethers';
interface TransferPayloadResponse {
abiMeta: {
tx: string; // Calldata (hex)
executionAddress: string; // MultiVault address
attachedValue: string; // Wei
abiMethod: 'deposit' | 'depositByNativeToken';
};
}
async function sendDepositToMultiVault(
provider: ethers.BrowserProvider,
payloadResponse: TransferPayloadResponse
): Promise<string> {
const signer = await provider.getSigner(); // Get signer from wallet (MetaMask, etc.)
const { abiMeta } = payloadResponse;
console.log(`Sending ${abiMeta.abiMethod} transaction...`);
console.log('Address:', abiMeta.executionAddress);
console.log('Amount:', abiMeta.attachedValue, 'wei');
// The API returns ready calldata — send as a raw transaction
const tx = await signer.sendTransaction({
to: abiMeta.executionAddress, // MultiVault address
data: abiMeta.tx, // Ready calldata from API
value: abiMeta.attachedValue, // Wei (gas or amount+gas for native)
});
console.log('Deposit transaction:', tx.hash);
const receipt = await tx.wait(1); // Wait for 1 confirmation
if (receipt?.status === 0) {
throw new Error('Deposit transaction failed');
}
console.log('Deposit confirmed:', receipt?.hash);
return receipt?.hash || tx.hash;
}How it works for different methods
The API always returns ready abiMeta.tx (calldata) and abiMeta.attachedValue (wei). The sendDepositToMultiVault function works the same for both methods — the difference is only in the values that the API sets automatically:
deposit(ERC-20):attachedValuecontains only gas for credit mode, calldata includes the token amountdepositByNativeToken(ETH/BNB/etc.):attachedValue= deposit amount + gas, calldata does not contain the amount (it is passed viamsg.value)
Usage example: deposit (ERC-20 token)
const provider = new ethers.BrowserProvider(window.ethereum);
// payloadResponse obtained in Step 1 (abiMethod = "deposit")
// 1. Approve (required for ERC-20)
await approveToken(
provider,
'0xdAC1...1ec7', // tokenAddress
payloadResponse.abiMeta.executionAddress, // MultiVault
'1000000' // amount
);
// 2. Deposit
const txHash = await sendDepositToMultiVault(provider, payloadResponse);
console.log('Transfer ID:', txHash);Usage example: depositByNativeToken (ETH, BNB, etc.)
const provider = new ethers.BrowserProvider(window.ethereum);
// payloadResponse obtained in Step 1 (abiMethod = "depositByNativeToken")
// Approve is NOT required for native currency
const txHash = await sendDepositToMultiVault(provider, payloadResponse);
console.log('Transfer ID:', txHash);After a successful transaction
- The deposit transaction is confirmed in the EVM network
- Transfer ID = Transaction Hash of this transaction
- Relay nodes detect the event and begin confirmation
- In credit mode: Gas Credit Backend automatically deploys the Event contract
- In non-credit mode: the user must deploy the Event themselves (Step 4)
Why Transfer ID is needed
- Tracking transfer status via
Bridge Aggregator API - Building
EventVoteDatafrom EVM receipt for non-credit mode (Step 4) - Diagnosing transfer issues
Step 3: Track transfer status
Goal: Get the transfer status and verify completion
Limitation for EVM→TVM
The transfer status is available via the API only after the transfer has been completed (released) in the TVM network. Until that moment, the API does not return data for this transfer. For non-credit mode, this means it only makes sense to check status after completing Step 4 (Event contract deployment).
API Endpoint
POST {BASE_URL}/transfers/status
Content-Type: application/jsonRequest parameters
interface StatusRequest {
evmTvm: {
transactionHashEvm: string; // EVM transaction hash from Step 2
dappChainId: number; // TVM chain ID of the target network
timestampCreatedFrom?: number; // Time filter (optional)
};
}| Parameter | Description |
|---|---|
transactionHashEvm | EVM transaction hash from Step 2 (Transfer ID) |
dappChainId | TVM chain ID of the target network |
timestampCreatedFrom | Minimum timestamp for filter (optional) |
Request example
curl -X POST '{BASE_URL}/transfers/status' \
-H 'Content-Type: application/json' \
-d '{
"evmTvm": {
"transactionHashEvm": "0x1234...abcd",
"dappChainId": -239
}
}'Response example (Completed)
{
"transfer": {
"evmTvm": {
"transferStatus": "Completed",
"timestampCreatedAt": 1704067200,
"outgoing": {
"chainId": 1,
"userAddress": "0x742d...Ab12",
"tokenAddress": "0xdAC1...1ec7",
"volumeExec": "0.999000",
"volumeUsdtExec": "0.999",
"feeVolumeExec": "0.001000",
"transactionHash": "0x1234...abcd"
},
"incoming": {
"tokenType": "Alien",
"contractAddress": "0:8888...1111",
"chainId": -239,
"userAddress": "0:1111...1111",
"tokenAddress": "0:2222...2222",
"proxyAddress": "0:4444...4444"
}
}
},
"updatedAt": 1704067260,
"proofPayload": null,
"notInstantTransfer": null
}Transfer statuses
| Status | Description |
|---|---|
Pending | Transfer is being processed. The Event contract has not yet received enough confirmations from relay nodes. |
Completed | Transfer completed successfully. Tokens received in the TVM network. |
Failed | Transfer failed. Log investigation and retry required. |
Step 4: Complete the transfer (Non-Credit mode)
Goal: Deploy the Event contract in TVM (only if useCredit=false)
When is this step required?
useCredit | Action |
|---|---|
true | Gas Credit Backend automatically deploys the Event contract. Skip Step 4. |
false | You manually collect data and deploy the Event contract. Execute Step 4. |
How it works
In non-credit mode, the data for deploying the Event contract is collected directly from the blockchain, not via the API. The process:
- Get the EventConfiguration address from bridge configurations
- Read the EVM receipt to extract event logs
- Convert EVM data to TVM format
- Send a
deployEventtransaction in TVM
Step 4.1: Get EventConfiguration address
The EventConfiguration address is determined via the bridge configurations endpoint:
GET {BASE_URL}/payload/configurations/allFrom the list of configurations, select the one matching these parameters:
| Parameter | Value | Description |
|---|---|---|
chainId | Source EVM network ID | Must match fromChainId |
meta.tokenType | Alien or Native | Token type from tokensMeta.sourceTokenType (Step 1) |
meta.direction | ToTvm | EVM→TVM direction |
interface BridgeConfiguration {
address: string; // EventConfiguration address in TVM
chainId: number; // EVM chain ID
flags: string; // Conversion flags
eventInitialBalance: string; // Minimum gas for Event deployment (nanoTON)
meta: {
tokenType: 'Alien' | 'Native';
direction: 'ToTvm' | 'ToEvm';
};
}
async function getConfiguration(
fromChainId: number,
tokenType: 'Alien' | 'Native'
): Promise<BridgeConfiguration> {
const response = await fetch(
'{BASE_URL}/payload/configurations/all'
);
const configurations: BridgeConfiguration[] = await response.json();
const config = configurations.find(item =>
item.chainId === fromChainId
&& item.meta.tokenType === tokenType
&& item.meta.direction === 'ToTvm'
);
if (!config) {
throw new Error(`Configuration not found for chainId=${fromChainId}, tokenType=${tokenType}`);
}
return config;
}Required ABIs
For Steps 4.2–4.3 you need two ABIs:
- EVM
MultiVaultevents - TVM
EventConfigurationcontract
// 1. MultiVault events ABI (EVM)
// Source: bridge-evm-contracts/contracts/interfaces/multivault/IMultiVaultFacetDepositEvents.sol
const MULTIVAULT_EVENTS_ABI = [
'event AlienTransfer(uint256 base_chainId, uint160 base_token, string name, string symbol, uint8 decimals, uint128 amount, int8 recipient_wid, uint256 recipient_addr, uint value, uint expected_gas, bytes payload)',
'event NativeTransfer(int8 native_wid, uint256 native_addr, uint128 amount, int8 recipient_wid, uint256 recipient_addr, uint value, uint expected_gas, bytes payload)',
];
// 2. EvmTvmEventConfiguration contract ABI (TVM) — minimal set: getDetails + deployEvent
// Source: bridge-ton-contracts/build/EvmTvmEventConfiguration.abi.json
const EVM_TVM_EVENT_CONFIGURATION_ABI = {
"ABI version": 2,
"version": "2.3",
"header": ["time", "expire"],
"functions": [
{
"name": "getDetails",
"inputs": [{"name":"answerId","type":"uint32"}],
"outputs": [
{"components":[{"name":"eventABI","type":"bytes"},{"name":"roundDeployer","type":"address"},{"name":"eventInitialBalance","type":"uint64"},{"name":"eventCode","type":"cell"}],"name":"_basicConfiguration","type":"tuple"},
{"components":[{"name":"chainId","type":"uint32"},{"name":"eventEmitter","type":"uint160"},{"name":"eventBlocksToConfirm","type":"uint16"},{"name":"proxy","type":"address"},{"name":"startBlockNumber","type":"uint32"},{"name":"endBlockNumber","type":"uint32"}],"name":"_networkConfiguration","type":"tuple"},
{"name":"_meta","type":"cell"}
]
},
{
"name": "deployEvent",
"inputs": [
{"components":[{"name":"eventTransaction","type":"uint256"},{"name":"eventIndex","type":"uint32"},{"name":"eventData","type":"cell"},{"name":"eventBlockNumber","type":"uint32"},{"name":"eventBlock","type":"uint256"}],"name":"_eventVoteData","type":"tuple"}
],
"outputs": []
}
],
"data": [],
"events": [],
"fields": []
} as const;Step 4.2: Build EventVoteData from EVM receipt
After the EVM transaction is confirmed (Step 2), you need to read the receipt and extract event logs.
// Common interface for both variants
interface EventVoteData {
eventTransaction: string; // uint256
eventIndex: string; // uint32
eventData: string; // cell (BOC)
eventBlockNumber: string; // uint32
eventBlock: string; // uint256
}import { ethers } from 'ethers';
import { mapEthBytesIntoTonCell } from 'eth-ton-abi-converter';
// MULTIVAULT_EVENTS_ABI — see "Required ABIs" above
async function buildEventVoteData(
evmProvider: ethers.BrowserProvider,
txHash: string, // Transfer ID from Step 2
configurationFlags: string // flags from /payload/configurations/all
): Promise<EventVoteData> {
// 1. Get EVM receipt
const receipt = await evmProvider.getTransactionReceipt(txHash);
if (!receipt) throw new Error('Transaction receipt not found');
// 2. Find AlienTransfer or NativeTransfer in logs
const iface = new ethers.Interface(MULTIVAULT_EVENTS_ABI);
let transferEvent: ethers.LogDescription | null = null;
let logIndex: number | undefined;
let rawLogData: string | undefined;
for (const log of receipt.logs) {
try {
const parsed = iface.parseLog({ topics: [...log.topics], data: log.data });
if (parsed && (parsed.name === 'AlienTransfer' || parsed.name === 'NativeTransfer')) {
transferEvent = parsed;
logIndex = log.index;
rawLogData = log.data;
break;
}
} catch { /* not our log, skip */ }
}
if (!transferEvent || logIndex === undefined || !rawLogData) {
throw new Error('AlienTransfer or NativeTransfer event not found in receipt');
}
// 3. Convert EVM event data to TVM cell
// ABI is taken from the EVM MultiVault contract (event field descriptions)
const eventABI = transferEvent.name === 'AlienTransfer'
? MULTIVAULT_EVENTS_ABI[0]
: MULTIVAULT_EVENTS_ABI[1];
const eventData = mapEthBytesIntoTonCell(eventABI, rawLogData, configurationFlags);
// 4. Build EventVoteData
return {
eventBlock: receipt.blockHash,
eventBlockNumber: receipt.blockNumber.toString(),
eventTransaction: receipt.hash,
eventIndex: logIndex.toString(),
eventData,
};
}import { ethers } from 'ethers';
import { mapEthBytesIntoTonCell } from 'eth-ton-abi-converter';
import { EverscaleStandaloneClient } from 'everscale-standalone-client';
import { Address, ProviderRpcClient } from 'everscale-inpage-provider';
// EVM_TVM_EVENT_CONFIGURATION_ABI — see "Required ABIs" above
async function buildEventVoteData(
evmProvider: ethers.BrowserProvider,
txHash: string, // Transfer ID from Step 2
configurationAddress: string, // address from Step 4.1
configurationFlags: string, // flags from Step 4.1
tvmRpcEndpoint: string // TVM network JRPC node URL
): Promise<EventVoteData> {
// 1. Connect to TVM and read eventABI from EventConfiguration
// eventABI — base64-encoded JSON EVM event ABI, stored in the contract
// Example: {"anonymous":false,"inputs":[...],"name":"AlienTransfer","type":"event"}
const tvmClient = new ProviderRpcClient({
fallback: () => EverscaleStandaloneClient.create({
connection: { type: 'jrpc', data: { endpoint: tvmRpcEndpoint } },
}),
});
await tvmClient.ensureInitialized();
const configContract = new tvmClient.Contract(
EVM_TVM_EVENT_CONFIGURATION_ABI,
new Address(configurationAddress)
);
const details = await configContract.methods.getDetails({ answerId: 0 }).call();
const eventABIEncoded = details._basicConfiguration.eventABI;
const eventABI = typeof Buffer !== 'undefined'
? Buffer.from(eventABIEncoded, 'base64').toString('utf8')
: atob(eventABIEncoded);
// 2. Get EVM receipt
const receipt = await evmProvider.getTransactionReceipt(txHash);
if (!receipt) throw new Error('Transaction receipt not found');
// 3. Find the needed log by eventABI from the contract
// eventABI contains the event name (AlienTransfer or NativeTransfer) —
// use it for log parsing, without hardcoding ABI
const iface = new ethers.Interface([JSON.parse(eventABI)]);
const eventName = JSON.parse(eventABI).name; // 'AlienTransfer' or 'NativeTransfer'
let logIndex: number | undefined;
let rawLogData: string | undefined;
for (const log of receipt.logs) {
try {
const parsed = iface.parseLog({ topics: [...log.topics], data: log.data });
if (parsed?.name === eventName) {
logIndex = log.index;
rawLogData = log.data;
break;
}
} catch { /* not our log, skip */ }
}
if (logIndex === undefined || !rawLogData) {
throw new Error(`${eventName} event not found in receipt`);
}
// 4. Convert EVM event data to TVM cell
// eventABI — from EventConfiguration contract (step 1 above)
// configurationFlags — from API /configurations/all (Step 4.1)
const eventData = mapEthBytesIntoTonCell(eventABI, rawLogData, configurationFlags);
// 5. Build EventVoteData
return {
eventBlock: receipt.blockHash,
eventBlockNumber: receipt.blockNumber.toString(),
eventTransaction: receipt.hash,
eventIndex: logIndex.toString(),
eventData,
};
}What is used from the EVM receipt
| Receipt field | Usage |
|---|---|
blockHash | eventVoteData.eventBlock |
blockNumber | eventVoteData.eventBlockNumber |
hash (transactionHash) | eventVoteData.eventTransaction — this is the Transfer ID from Step 2 |
logs[i].index (logIndex) | eventVoteData.eventIndex |
logs[i].data | Converted to TVM cell → eventVoteData.eventData |
Step 4.3: Deploy Event contract
Encode the deployEvent call and send the transaction:
import TonConnectUI from '@tonconnect/ui';
import { beginCell, Cell } from '@ton/core';
// Compute function ID according to TVM ABI v2 rules:
// SHA-256 of the function signature, first 4 bytes (big-endian uint32)
async function computeTvmFunctionId(signature: string): Promise<number> {
const data = new TextEncoder().encode(signature);
const hash = await crypto.subtle.digest('SHA-256', data);
return new DataView(hash).getUint32(0, false);
}
async function deployEventContract(
tonConnectUI: TonConnectUI,
configurationAddress: string,
eventVoteData: EventVoteData,
eventInitialBalance: string
): Promise<string> {
// 1. Compute function ID for deployEvent
// Signature: name(input_parameter_types)(return_types)
const functionId = await computeTvmFunctionId(
'deployEvent((uint256,uint32,cell,uint32,uint256))()'
);
// 2. Encode deployEvent call into a cell
// Field order — from ABI: eventTransaction, eventIndex, eventData, eventBlockNumber, eventBlock
const eventDataCell = Cell.fromBase64(eventVoteData.eventData);
const payload = beginCell()
.storeUint(functionId, 32)
.storeUint(BigInt(eventVoteData.eventTransaction), 256)
.storeUint(Number(eventVoteData.eventIndex), 32)
.storeRef(eventDataCell) // cell is stored as a reference
.storeUint(Number(eventVoteData.eventBlockNumber), 32)
.storeUint(BigInt(eventVoteData.eventBlock), 256)
.endCell()
.toBoc()
.toString('base64');
// 3. Calculate gas (eventInitialBalance + buffer)
const expectedNatives = (
BigInt(eventInitialBalance) + BigInt(500_000_000)
).toString();
// 4. Send via TonConnect
const result = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 600,
messages: [{
address: configurationAddress,
amount: expectedNatives,
payload,
}],
});
console.log('Event deployed, BOC:', result.boc);
return result.boc;
}import { Address, ProviderRpcClient } from 'everscale-inpage-provider';
// EVM_TVM_EVENT_CONFIGURATION_ABI — see "Required ABIs" above
async function deployEventContract(
tvmClient: ProviderRpcClient,
senderAddress: string, // TVM sender address (user's wallet)
configurationAddress: string,
eventVoteData: EventVoteData,
eventInitialBalance: string
): Promise<string> {
const configContract = new tvmClient.Contract(
EVM_TVM_EVENT_CONFIGURATION_ABI,
new Address(configurationAddress)
);
// 1. Calculate gas (eventInitialBalance + buffer)
const amount = (
BigInt(eventInitialBalance) + BigInt(500_000_000)
).toString();
// 2. Send transaction via TVM SDK
// .send() automatically computes function ID, encodes parameters,
// and sends the transaction on behalf of the connected wallet
const { transaction } = await configContract.methods
.deployEvent({ _eventVoteData: eventVoteData })
.send({
from: new Address(senderAddress),
amount,
bounce: true,
});
console.log('Event deployed, tx hash:', transaction.id.hash);
return transaction.id.hash;
}Reusing the TVM client
If you used the [TVM SDK] variant in Step 4.2, tvmClient is already created — pass it to deployEventContract() without creating it again.
Event contract lifecycle
After the Event contract is deployed, the following happens:
- Event contract initializes with transfer data (token address, amount, recipient)
- Event requests configuration from EvmTvmEventConfiguration and relay node public keys
- Relay nodes confirm the event — each relay calls
confirm()and records a vote - Consensus reached — when
confirms >= requiredVotes(whererequiredVotes = keys.length * 2/3 + 1), the Event transitions to Confirmed - Event calls callback to ProxyMultiVaultAlien/ProxyMultiVaultNative
- Proxy finalizes the transfer — mints/unlocks tokens for the user
- Transfer completed — status becomes
Completed
Search transfers
API Endpoint
POST {BASE_URL}/transfers/search
Content-Type: application/jsonRequest example
curl -X POST '{BASE_URL}/transfers/search' \
-H 'Content-Type: application/json' \
-d '{
"transferKinds": ["EvmToTvm"],
"evmUserAddress": "0x742d...Ab12",
"limit": 10,
"offset": 0,
"isNeedTotalCount": true
}'Filter parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
transferKinds | string[] | ❌ | ["EvmToTvm"] for EVM→TVM transfers |
evmUserAddress | string | ❌ | Filter by EVM sender address |
tvmUserAddress | string | ❌ | Filter by TVM recipient address |
statuses | string[] | ❌ | Pending, Completed, Failed |
fromEvmChainId | number | ❌ | Filter by source EVM network |
toTvmChainId | number | ❌ | Filter by target TVM network |
limit | number | ✅ | Record limit |
offset | number | ✅ | Offset |
isNeedTotalCount | boolean | ✅ | Whether to return the total count |
Transfer modes: Credit vs Non-Credit
| Aspect | Credit (useCredit=true) | Non-Credit (useCredit=false) |
|---|---|---|
| Gas in TVM | Paid by Gas Credit Backend | Paid by the user |
| Automation | Fully automatic | Requires manual Event deployment |
| Speed | Faster (automatic deployment) | Depends on the user |
| Fee | Included in feesMeta.amount | Bridge fee only |
| Recommendation | For regular users | For advanced users/automation |
Credit Mode (useCredit=true)
- Gas Credit Backend automatically deploys the Event contract in TVM
- Gas is paid from your transfer fee
- The user does not need to intervene after deposit in EVM
- The transfer completes automatically
Non-Credit Mode (useCredit=false)
- You are responsible for deploying the Event contract
- Deployment data is collected from EVM receipt + TVM EventConfiguration contract
- EVM event logs are converted to TVM format (
eth-ton-abi-converter) - The
deployEventtransaction is sent via TonConnect or TVM SDK - Cheaper, but requires additional steps
Full example: EVM→TVM transfer
import { ethers } from 'ethers';
import TonConnectUI from '@tonconnect/ui';
// Production: https://tetra-history-api.chainconnect.com/v2
// Testnet: https://history-api-test.chainconnect.com/v2
const API_BASE = '{BASE_URL}';
// Configuration
const CONFIG = {
evmChainId: 1,
tvmChainId: -239,
tokenAddress: '0xdAC1...1ec7', // USDT
amount: '1000000', // 1 USDT
};
// ABI — see the "Required ABIs" section:
// MULTIVAULT_EVENTS_ABI — MultiVault events ABI (EVM)
// EVM_TVM_EVENT_CONFIGURATION_ABI — EvmTvmEventConfiguration contract ABI (TVM)
// --- Helper functions (defined in Steps 2–4) ---
// Step 2.1: approveToken(provider, tokenAddress, spenderAddress, amount)
// Step 2.2: sendDepositToMultiVault(provider, payloadResponse)
// Step 4.1: getConfiguration(fromChainId, tokenType)
// Step 4.2: buildEventVoteData(evmProvider, txHash, configurationFlags)
// Step 4.3: computeTvmFunctionId(signature), deployEventContract(tonConnectUI, ...)
async function evmToTvmTransfer(
evmProvider: ethers.BrowserProvider,
tonConnectUI: TonConnectUI,
params: {
fromChainId: number;
toChainId: number;
tokenAddress: string;
recipientAddress: string;
amount: string;
useCredit: boolean;
}
) {
const signer = await evmProvider.getSigner();
const senderAddress = await signer.getAddress();
// === Step 1: Build Payload ===
console.log('[Step 1] Building payload...');
const payloadResponse = await fetch(`${API_BASE}/payload/build`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...params,
senderAddress,
}),
}).then(r => r.json());
console.log('Payload built:', {
transferKind: payloadResponse.transferKind,
tokenAmount: payloadResponse.tokenAmount,
fee: payloadResponse.feesMeta?.amount,
method: payloadResponse.abiMeta.abiMethod,
});
const { abiMeta } = payloadResponse;
// === Step 2: Approve + Deposit ===
// This example uses an ERC-20 token (USDT), so abiMethod = "deposit"
// and approve is required. For native currency (ETH, BNB) approve is not needed.
if (abiMeta.abiMethod === 'deposit') {
console.log('\n[Step 2.1] Approving ERC-20 token...');
await approveToken(
evmProvider,
params.tokenAddress,
abiMeta.executionAddress,
params.amount
);
}
console.log('\n[Step 2.2] Sending deposit...');
const txHash = await sendDepositToMultiVault(evmProvider, payloadResponse);
console.log('Deposit confirmed. Transfer ID:', txHash);
// === Step 3: Check status ===
console.log('\n[Step 3] Checking transfer status...');
const statusResponse = await fetch(`${API_BASE}/transfers/status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
evmTvm: {
transactionHashEvm: txHash,
dappChainId: params.toChainId,
},
}),
}).then(r => r.json());
const status = statusResponse.transfer?.evmTvm?.transferStatus;
console.log('Current status:', status);
// === Step 4: Deploy Event (non-credit only) ===
if (!params.useCredit) {
console.log('\n[Step 4] Deploying Event contract (non-credit)...');
// 4.1: Get configuration (address, flags, eventInitialBalance)
const tokenType = payloadResponse.tokensMeta.sourceTokenType; // 'Alien' or 'Native'
const configuration = await getConfiguration(params.fromChainId, tokenType);
// 4.2: Build EventVoteData from EVM receipt
const eventVoteData = await buildEventVoteData(
evmProvider, txHash, configuration.flags
);
// 4.3: Deploy Event contract via TonConnect
await deployEventContract(
tonConnectUI,
configuration.address,
eventVoteData,
configuration.eventInitialBalance
);
console.log('Event deployed successfully');
} else {
console.log('\n[Step 4] Skipped — Credit Backend deploys Event automatically');
}
console.log('\nTransfer completed!');
return statusResponse;
}
// --- Usage ---
const evmProvider = new ethers.BrowserProvider(window.ethereum);
// 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',
});
await evmToTvmTransfer(evmProvider, tonConnectUI, {
fromChainId: CONFIG.evmChainId,
toChainId: CONFIG.tvmChainId,
tokenAddress: CONFIG.tokenAddress,
recipientAddress: '0:1111...1111',
amount: CONFIG.amount,
useCredit: true, // false for non-credit (will require Step 4)
});import { ethers } from 'ethers';
import { EverscaleStandaloneClient } from 'everscale-standalone-client';
import { Address, ProviderRpcClient } from 'everscale-inpage-provider';
// Production: https://tetra-history-api.chainconnect.com/v2
// Testnet: https://history-api-test.chainconnect.com/v2
const API_BASE = '{BASE_URL}';
const TVM_RPC_ENDPOINT = 'https://jrpc-ton.broxus.com'; // TVM network JRPC node
// Configuration
const CONFIG = {
evmChainId: 1,
tvmChainId: -239,
tokenAddress: '0xdAC1...1ec7', // USDT
amount: '1000000', // 1 USDT
};
// ABI — see the "Required ABIs" section:
// MULTIVAULT_EVENTS_ABI — MultiVault events ABI (EVM)
// EVM_TVM_EVENT_CONFIGURATION_ABI — EvmTvmEventConfiguration contract ABI (TVM)
// --- Helper functions (defined in Steps 2–4) ---
// Step 2.1: approveToken(provider, tokenAddress, spenderAddress, amount)
// Step 2.2: sendDepositToMultiVault(provider, payloadResponse)
// Step 4.1: getConfiguration(fromChainId, tokenType)
// Step 4.2: buildEventVoteData(evmProvider, txHash, configurationAddress, configurationFlags, tvmRpcEndpoint)
// Step 4.3: deployEventContract(tvmClient, senderAddress, configurationAddress, eventVoteData, eventInitialBalance)
async function evmToTvmTransfer(
evmProvider: ethers.BrowserProvider,
tvmSenderAddress: string, // User's TVM wallet address
params: {
fromChainId: number;
toChainId: number;
tokenAddress: string;
recipientAddress: string;
amount: string;
useCredit: boolean;
}
) {
const signer = await evmProvider.getSigner();
const senderAddress = await signer.getAddress();
// === Step 1: Build Payload ===
console.log('[Step 1] Building payload...');
const payloadResponse = await fetch(`${API_BASE}/payload/build`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...params,
senderAddress,
}),
}).then(r => r.json());
console.log('Payload built:', {
transferKind: payloadResponse.transferKind,
tokenAmount: payloadResponse.tokenAmount,
fee: payloadResponse.feesMeta?.amount,
method: payloadResponse.abiMeta.abiMethod,
});
const { abiMeta } = payloadResponse;
// === Step 2: Approve + Deposit ===
// This example uses an ERC-20 token (USDT), so abiMethod = "deposit"
// and approve is required. For native currency (ETH, BNB) approve is not needed.
if (abiMeta.abiMethod === 'deposit') {
console.log('\n[Step 2.1] Approving ERC-20 token...');
await approveToken(
evmProvider,
params.tokenAddress,
abiMeta.executionAddress,
params.amount
);
}
console.log('\n[Step 2.2] Sending deposit...');
const txHash = await sendDepositToMultiVault(evmProvider, payloadResponse);
console.log('Deposit confirmed. Transfer ID:', txHash);
// === Step 3: Check status ===
console.log('\n[Step 3] Checking transfer status...');
const statusResponse = await fetch(`${API_BASE}/transfers/status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
evmTvm: {
transactionHashEvm: txHash,
dappChainId: params.toChainId,
},
}),
}).then(r => r.json());
const status = statusResponse.transfer?.evmTvm?.transferStatus;
console.log('Current status:', status);
// === Step 4: Deploy Event (non-credit only) ===
if (!params.useCredit) {
console.log('\n[Step 4] Deploying Event contract (non-credit)...');
// 4.1: Get configuration (address, flags, eventInitialBalance)
const tokenType = payloadResponse.tokensMeta.sourceTokenType; // 'Alien' or 'Native'
const configuration = await getConfiguration(params.fromChainId, tokenType);
// Create TVM client (reused in Steps 4.2 and 4.3)
const tvmClient = new ProviderRpcClient({
fallback: () => EverscaleStandaloneClient.create({
connection: { type: 'jrpc', data: { endpoint: TVM_RPC_ENDPOINT } },
}),
});
await tvmClient.ensureInitialized();
// 4.2: Build EventVoteData from EVM receipt (eventABI is read from the contract)
const eventVoteData = await buildEventVoteData(
evmProvider, txHash, configuration.address, configuration.flags, TVM_RPC_ENDPOINT
);
// 4.3: Deploy Event contract via TVM SDK
await deployEventContract(
tvmClient,
tvmSenderAddress,
configuration.address,
eventVoteData,
configuration.eventInitialBalance
);
console.log('Event deployed successfully');
} else {
console.log('\n[Step 4] Skipped — Credit Backend deploys Event automatically');
}
console.log('\nTransfer completed!');
return statusResponse;
}
// --- Usage ---
const evmProvider = new ethers.BrowserProvider(window.ethereum);
const tvmSenderAddress = '0:1111...1111'; // TVM address of the connected wallet
await evmToTvmTransfer(evmProvider, tvmSenderAddress, {
fromChainId: CONFIG.evmChainId,
toChainId: CONFIG.tvmChainId,
tokenAddress: CONFIG.tokenAddress,
recipientAddress: '0:1111...1111',
amount: CONFIG.amount,
useCredit: true, // false for non-credit (will require Step 4)
});Error reference
EVM errors during Deposit
| Error | Cause | Solution |
|---|---|---|
Msg value to low | msg.value is too low for native currency | Increase abiMeta.attachedValue |
Deposit: limits violated | Deposit amount limit exceeded | Wait for limit reset (24h sliding window) or reduce the amount |
Deposit amount too is large | Amount >= 2^128 | Send multiple smaller transfers |
Pending: already filled | Attempt to fill a closed pending | Use a different pending withdrawal |
Pending: wrong token | Token does not match the expected one | Check tokenAddress in the request |
Pending: deposit insufficient | Amount insufficient to cover the pending | Increase amount in the request |
Emergency: shutdown | Bridge is in emergency shutdown mode | Wait for the mode to be deactivated |
Tokens: token is blacklisted | Token is blacklisted (for ERC-20) | Contact the team, token is blocked |
Tokens: weth is blacklisted | WETH is blacklisted (for native currency) | Contact the team |
Tokens: invalid token meta | Invalid token metadata (decimals/symbol/name) | Contact the team |
Insufficient allowance | Insufficient approve | Execute approve for the required amount |
ReentrancyGuard: reentrant call | Reentrancy call attempt | System error, contact the team |
TVM errors during Event Deploy (Non-Credit)
| Error (code) | Cause | Solution |
|---|---|---|
| 2213 | msg.value < eventInitialBalance | Increase gas amount (use eventInitialBalance from configuration + buffer) |
| 2105 | Event configuration is not registered | System error, contact the team |
| 2103 | Event configuration is deactivated | System error, contact the team |