Skip to content

EVM→TVM Transfer

Overview

An EVM→TVM transfer allows you to send tokens from an EVM network to a TVM network. The process includes:

  1. Deposit to MultiVault — the user sends tokens to the EVM contract (lock for Alien, burn for Native)
  2. Event indexing — relay nodes and indexers track the deposit
  3. Event contract deployment — a relay node creates an Event contract in the TVM network
  4. Relay node consensus — relay nodes call confirm() (no signatures, only voting)
  5. Callback to Proxy — when enough votes are collected, the Event calls a callback to the Proxy contract
  6. 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, where requiredVotes = 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

TypeEVM operationTVM operationDescription
Alienlock in MultiVaultmint alienToken from EVM is locked, an alien representation is created in TVM
Nativeburn in MultiVaultunlock nativeToken is returned from EVM, unlocked in TVM

API Base URL

All API requests in this guide use one of two environments:

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

Endpoints used in the guide:

EndpointMethodStep
/payload/buildPOSTStep 1 — build payload
/transfers/statusPOSTStep 3 — check status
/payload/configurations/allGETStep 4.1 — bridge configurations
/transfers/searchPOSTSearch 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/json

Request parameters

typescript
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)
  };
}
ParameterTypeRequiredDescription
fromChainIdnumberChain ID of the source EVM network
toChainIdnumberChain ID of the target TVM network
tokenAddressstringERC-20 token address in EVM (format 0x... lowercase)
recipientAddressstringRecipient address in TVM (format 0:...)
amountstringAmount in smallest units (integer as string, e.g. "1000000" for 1 USDT with 6 decimals)
senderAddressstringSender address in EVM (format 0x... lowercase)
useCreditbooleanIf true — Gas Credit Backend automatically deploys the Event contract in TVM and pays gas. Default: false
remainingGasTostringAddress for returning remaining gas (used when useCredit=false)
callbackobjectCallback parameters (EVM contract address to call after transfer completion)
tokenBalance.nativeTokenAmountstring⚠️Gas token balance (native currency). Required when sending gas token
tokenBalance.wrappedNativeTokenAmountstring⚠️Wrapped gas token balance. Required when sending gas token

Request example

bash
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

json
{
  "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

FieldDescriptionUsage
transferKindTransfer type (EvmToTvm)Direction confirmation
tokensMeta.targetToken.tokenTypeToken type in the target network (Alien or Native)Token type information
tokenAmountFinal amount after fee deduction (in nano-units)Amount the user will receive
feesMeta.amountBridge fee amount (in nano-units)Fee information
feesMeta.numerator / denominatorFee fraction (numerator/denominator)Fee percentage calculation
abiMeta.executionAddressMultiVault contract address in EVMEVM transaction recipient (Step 2)
abiMeta.txTransaction calldata (hex)EVM transaction data (Step 2)
abiMeta.attachedValueAmount of ETH/BNB/etc to send (in wei)Transaction msg.value (Step 2)
abiMeta.abiMethodContract method (deposit or depositByNativeToken)Method information
abiMeta.paramsCall parameters (JSON)For building calldata
trackingMeta.sourceMultivaultMultiVault address in EVMInformational
trackingMeta.targetProxyProxy contract address in TVMInformational

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 fieldDescriptionUsage
abiMeta.executionAddressMultiVault contract addressto field in the EVM transaction
abiMeta.txTransaction calldata (hex)data field in the EVM transaction
abiMeta.attachedValueAmount of wei to sendvalue field in the EVM transaction
abiMeta.abiMethoddeposit or depositByNativeTokenDetermines whether approve is needed (Step 2.1)

Deposit methods

MethodWhen to usemsg.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:

typescript
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

typescript
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): attachedValue contains only gas for credit mode, calldata includes the token amount
  • depositByNativeToken (ETH/BNB/etc.): attachedValue = deposit amount + gas, calldata does not contain the amount (it is passed via msg.value)

Usage example: deposit (ERC-20 token)

typescript
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.)

typescript
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

  1. The deposit transaction is confirmed in the EVM network
  2. Transfer ID = Transaction Hash of this transaction
  3. Relay nodes detect the event and begin confirmation
  4. In credit mode: Gas Credit Backend automatically deploys the Event contract
  5. 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 EventVoteData from 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/json

Request parameters

typescript
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)
  };
}
ParameterDescription
transactionHashEvmEVM transaction hash from Step 2 (Transfer ID)
dappChainIdTVM chain ID of the target network
timestampCreatedFromMinimum timestamp for filter (optional)

Request example

bash
curl -X POST '{BASE_URL}/transfers/status' \
  -H 'Content-Type: application/json' \
  -d '{
    "evmTvm": {
      "transactionHashEvm": "0x1234...abcd",
      "dappChainId": -239
    }
  }'

Response example (Completed)

json
{
  "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

StatusDescription
PendingTransfer is being processed. The Event contract has not yet received enough confirmations from relay nodes.
CompletedTransfer completed successfully. Tokens received in the TVM network.
FailedTransfer 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?

useCreditAction
trueGas Credit Backend automatically deploys the Event contract. Skip Step 4.
falseYou 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:

  1. Get the EventConfiguration address from bridge configurations
  2. Read the EVM receipt to extract event logs
  3. Convert EVM data to TVM format
  4. Send a deployEvent transaction in TVM

Step 4.1: Get EventConfiguration address

The EventConfiguration address is determined via the bridge configurations endpoint:

GET {BASE_URL}/payload/configurations/all

From the list of configurations, select the one matching these parameters:

ParameterValueDescription
chainIdSource EVM network IDMust match fromChainId
meta.tokenTypeAlien or NativeToken type from tokensMeta.sourceTokenType (Step 1)
meta.directionToTvmEVM→TVM direction
typescript
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 MultiVault events
  • TVM EventConfiguration contract
typescript
// 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.

typescript
// Common interface for both variants
interface EventVoteData {
  eventTransaction: string;  // uint256
  eventIndex: string;        // uint32
  eventData: string;         // cell (BOC)
  eventBlockNumber: string;  // uint32
  eventBlock: string;        // uint256
}
typescript
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,
  };
}
typescript
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 fieldUsage
blockHasheventVoteData.eventBlock
blockNumbereventVoteData.eventBlockNumber
hash (transactionHash)eventVoteData.eventTransaction — this is the Transfer ID from Step 2
logs[i].index (logIndex)eventVoteData.eventIndex
logs[i].dataConverted to TVM cell → eventVoteData.eventData

Step 4.3: Deploy Event contract

Encode the deployEvent call and send the transaction:

typescript
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;
}
typescript
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:

  1. Event contract initializes with transfer data (token address, amount, recipient)
  2. Event requests configuration from EvmTvmEventConfiguration and relay node public keys
  3. Relay nodes confirm the event — each relay calls confirm() and records a vote
  4. Consensus reached — when confirms >= requiredVotes (where requiredVotes = keys.length * 2/3 + 1), the Event transitions to Confirmed
  5. Event calls callback to ProxyMultiVaultAlien/ProxyMultiVaultNative
  6. Proxy finalizes the transfer — mints/unlocks tokens for the user
  7. Transfer completed — status becomes Completed

Search transfers

API Endpoint

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

Request example

bash
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

ParameterTypeRequiredDescription
transferKindsstring[]["EvmToTvm"] for EVM→TVM transfers
evmUserAddressstringFilter by EVM sender address
tvmUserAddressstringFilter by TVM recipient address
statusesstring[]Pending, Completed, Failed
fromEvmChainIdnumberFilter by source EVM network
toTvmChainIdnumberFilter by target TVM network
limitnumberRecord limit
offsetnumberOffset
isNeedTotalCountbooleanWhether to return the total count

Transfer modes: Credit vs Non-Credit

AspectCredit (useCredit=true)Non-Credit (useCredit=false)
Gas in TVMPaid by Gas Credit BackendPaid by the user
AutomationFully automaticRequires manual Event deployment
SpeedFaster (automatic deployment)Depends on the user
FeeIncluded in feesMeta.amountBridge fee only
RecommendationFor regular usersFor 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 deployEvent transaction is sent via TonConnect or TVM SDK
  • Cheaper, but requires additional steps

Full example: EVM→TVM transfer

typescript
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)
});
typescript
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

ErrorCauseSolution
Msg value to lowmsg.value is too low for native currencyIncrease abiMeta.attachedValue
Deposit: limits violatedDeposit amount limit exceededWait for limit reset (24h sliding window) or reduce the amount
Deposit amount too is largeAmount >= 2^128Send multiple smaller transfers
Pending: already filledAttempt to fill a closed pendingUse a different pending withdrawal
Pending: wrong tokenToken does not match the expected oneCheck tokenAddress in the request
Pending: deposit insufficientAmount insufficient to cover the pendingIncrease amount in the request
Emergency: shutdownBridge is in emergency shutdown modeWait for the mode to be deactivated
Tokens: token is blacklistedToken is blacklisted (for ERC-20)Contact the team, token is blocked
Tokens: weth is blacklistedWETH is blacklisted (for native currency)Contact the team
Tokens: invalid token metaInvalid token metadata (decimals/symbol/name)Contact the team
Insufficient allowanceInsufficient approveExecute approve for the required amount
ReentrancyGuard: reentrant callReentrancy call attemptSystem error, contact the team

TVM errors during Event Deploy (Non-Credit)

Error (code)CauseSolution
2213msg.value < eventInitialBalanceIncrease gas amount (use eventInitialBalance from configuration + buffer)
2105Event configuration is not registeredSystem error, contact the team
2103Event configuration is deactivatedSystem error, contact the team

ChainConnect Bridge Documentation