Skip to content

Трансфер EVM→TVM

Обзор

Трансфер EVM→TVM позволяет отправить токены из EVM сети в TVM сеть. Процесс включает:

  1. Депозит в MultiVault — пользователь отправляет токены в EVM контракт (lock для Alien, burn для Native)
  2. Индексация события — relay-ноды и индексеры отслеживают депозит
  3. Деплой Event-контракта — relay-нода создаёт Event-контракт в TVM сети
  4. Консенсус relay-нод — relay-ноды вызывают confirm() (без подписей, только голосование)
  5. Callback в Proxy — при достаточном количестве голосов Event вызывает callback в Proxy-контракт
  6. Финальное действие — Proxy минтит/разлокивает токены пользователю в TVM сети

Механизм консенсуса (EVM→TVM)

Для EVM→TVM консенсус достигается через голосование relay-нод в TVM сети:

  • Relay-ноды вызывают confirm(voteReceiver) через внешние сообщения TVM (подписанные ключом relay)
  • Relay идентифицируется через msg.pubkey() из подписи внешнего сообщения
  • Консенсус: confirms >= requiredVotes, где requiredVotes = keys.length * 2/3 + 1
  • После подтверждения Event-контракт сразу вызывает callback в Proxy-контракт
  • Proxy выполняет mint/unlock токенов в TVM сети

Явные криптографические подписи не передаются в параметрах — аутентификация relay происходит через встроенный механизм подписи внешних сообщений TVM.

Типы токенов

ТипОперация EVMОперация TVMОписание
Alienlock в MultiVaultmint alienТокен из EVM блокируется, в TVM создаётся alien-представление
Nativeburn в MultiVaultunlock nativeТокен возвращается из EVM, в TVM разблокируется

API Base URL

Все API-запросы в этом гайде используют один из двух окружений:

ОкружениеBase URL
Productionhttps://tetra-history-api.chainconnect.com/v2
Testnethttps://history-api-test.chainconnect.com/v2

Эндпоинты, используемые в гайде:

ЭндпоинтМетодШаг
/payload/buildPOSTШаг 1 — сборка payload
/transfers/statusPOSTШаг 3 — проверка статуса
/payload/configurations/allGETШаг 4.1 — конфигурации моста
/transfers/searchPOSTПоиск трансферов

Шаг 1: Подготовка трансфера (Build Payload)

Цель: Получить payload для отправки транзакции в EVM сеть

API Endpoint

POST {BASE_URL}/payload/build
Content-Type: application/json

Параметры запроса

typescript
interface TransferPayloadRequest {
  fromChainId: number;          // Chain ID EVM сети отправления
  toChainId: number;            // Chain ID TVM сети назначения
  tokenAddress: string;         // Адрес токена в EVM (hex формат 0x...)
  recipientAddress: string;     // Адрес получателя в TVM (формат 0:...)
  amount: string;               // Сумма в nano-единицах (integer string)
  senderAddress: string;        // Адрес отправителя в EVM (hex формат 0x...)

  useCredit?: boolean;          // true = Credit Backend оплачивает газ. Default: false
  remainingGasTo?: string;      // Куда вернуть остаток газа в TVM (игнорируется при useCredit=true)
  payload?: string;             // Дополнительный payload (base64)
  callback?: CallbackRequest;   // Callback при завершении
  evmChainId?: number;          // Явное указание EVM chain ID
  tokenBalance?: {              // Для native currency (wrapped native token)
    nativeTokenAmount: string;  // Баланс native currency (для wrapped native token трансферов) 
    wrappedNativeTokenAmount: string; //  Баланс wrapped native (для wrapped native token трансферов) 
  };
}
ПараметрТипОбязательныйОписание
fromChainIdnumberChain ID EVM сети отправления
toChainIdnumberChain ID TVM сети назначения
tokenAddressstringАдрес ERC-20 токена в EVM (формат 0x... lowercase)
recipientAddressstringАдрес получателя в TVM (формат 0:...)
amountstringСумма в минимальных единицах (целое число как строка, напр. "1000000" для 1 USDT с 6 decimals)
senderAddressstringАдрес отправителя в EVM (формат 0x... lowercase)
useCreditbooleanЕсли true — Gas Credit Backend автоматически деплоит Event-контракт в TVM и оплачивает газ. Default: false
remainingGasTostringАдрес для возврата остатка газа (используется если useCredit=false)
callbackobjectПараметры callback (адрес EVM контракта для вызова после завершения трансфера)
tokenBalance.nativeTokenAmountstring⚠️Баланс газового токена (native currency). Обязателен при отправке газового токена
tokenBalance.wrappedNativeTokenAmountstring⚠️Баланс wrapped версии газового токена. Обязателен при отправке газового токена

Пример запроса

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
  }'

Пример ответа

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
}

Поля ответа

ПолеОписаниеИспользование
transferKindТип трансфера (EvmToTvm)Подтверждение направления
tokensMeta.targetToken.tokenTypeТип токена в целевой сети (Alien или Native)Информация о типе токена
tokenAmountИтоговая сумма после вычета комиссии (в nano-единицах)Сумма, которую получит пользователь
feesMeta.amountРазмер комиссии моста (в nano-единицах)Информация о комиссии
feesMeta.numerator / denominatorДробь комиссии (numerator/denominator)Расчёт процента комиссии
abiMeta.executionAddressАдрес MultiVault контракта в EVMПолучатель EVM транзакции (Шаг 2)
abiMeta.txCalldata транзакции (hex)Data EVM транзакции (Шаг 2)
abiMeta.attachedValueКоличество ETH/BNB/etc для отправки (в wei)msg.value транзакции (Шаг 2)
abiMeta.abiMethodМетод контракта (deposit или depositByNativeToken)Информация о методе
abiMeta.paramsПараметры вызова (JSON)Для формирования calldata
trackingMeta.sourceMultivaultАдрес MultiVault в EVMИнформационное
trackingMeta.targetProxyАдрес Proxy контракта в TVMИнформационное

Шаг 2: Отправка транзакции в EVM

Цель: Выполнить депозит токенов в MultiVault (lock для Alien, burn для Native) и получить Transaction Hash

Что потребуется из Шага 1

Поле из ответаОписаниеИспользование
abiMeta.executionAddressАдрес MultiVault контрактаПоле to в EVM транзакции
abiMeta.txCalldata транзакции (hex)Поле data в EVM транзакции
abiMeta.attachedValueКоличество wei для отправкиПоле value в EVM транзакции
abiMeta.abiMethoddeposit или depositByNativeTokenОпределяет нужен ли approve (Шаг 2.1)

Методы депозита

МетодКогда использоватьmsg.value
deposit()Для ERC-20 токенов (Alien)Только газ для credit режима
depositByNativeToken()Для native currency (ETH, BNB и др.)Сумма депозита + газ

Шаг 2.1: Approve токена (для ERC-20)

Перед депозитом ERC-20 токена необходимо разрешить MultiVault списать токены:

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

  // Проверяем текущий allowance
  const currentAllowance = await token.allowance(
    await signer.getAddress(),
    spenderAddress
  );

  if (currentAllowance >= BigInt(amount)) {
    console.log('Allowance достаточен, approve не требуется');
    return '';
  }

  // Выполняем approve
  const tx = await token.approve(spenderAddress, amount);
  const receipt = await tx.wait();
  console.log('Approve подтверждён:', receipt.hash);

  return receipt.hash;
}

Шаг 2.2: Отправка депозита

typescript
import { ethers } from 'ethers';

interface TransferPayloadResponse {
  abiMeta: {
    tx: string;               // Calldata (hex)
    executionAddress: string;  // MultiVault адрес
    attachedValue: string;     // Wei
    abiMethod: 'deposit' | 'depositByNativeToken';
  };
}

async function sendDepositToMultiVault(
  provider: ethers.BrowserProvider,
  payloadResponse: TransferPayloadResponse
): Promise<string> {
  const signer = await provider.getSigner(); // Получаем signer из кошелька (MetaMask и т.д.)
  const { abiMeta } = payloadResponse;

  console.log(`Отправка ${abiMeta.abiMethod} транзакции...`);
  console.log('Адрес:', abiMeta.executionAddress);
  console.log('Сумма:', abiMeta.attachedValue, 'wei');

  // API возвращает готовый calldata — отправляем как raw транзакцию
  const tx = await signer.sendTransaction({
    to: abiMeta.executionAddress,   // MultiVault адрес
    data: abiMeta.tx,               // Готовый calldata из API
    value: abiMeta.attachedValue,   // Wei (газ или сумма+газ для native)
  });

  console.log('Deposit транзакция:', tx.hash);
  const receipt = await tx.wait(1); // Ожидаем 1 подтверждение

  if (receipt?.status === 0) {
    throw new Error('Транзакция депозита не удалась');
  }

  console.log('Депозит подтверждён:', receipt?.hash);
  return receipt?.hash || tx.hash;
}

Как работает для разных методов

API всегда возвращает готовые abiMeta.tx (calldata) и abiMeta.attachedValue (wei). Функция sendDepositToMultiVault работает одинаково для обоих методов — разница только в значениях, которые API подставляет автоматически:

  • deposit (ERC-20): attachedValue содержит только газ для credit режима, calldata включает сумму токена
  • depositByNativeToken (ETH/BNB/etc.): attachedValue = сумма депозита + газ, calldata не содержит сумму (она передаётся через msg.value)

Пример использования: deposit (ERC-20 токен)

typescript
const provider = new ethers.BrowserProvider(window.ethereum);

// payloadResponse получен на Шаге 1 (abiMethod = "deposit")

// 1. Approve (обязателен для 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);

Пример использования: depositByNativeToken (ETH, BNB и др.)

typescript
const provider = new ethers.BrowserProvider(window.ethereum);

// payloadResponse получен на Шаге 1 (abiMethod = "depositByNativeToken")
// Approve НЕ требуется для native currency

const txHash = await sendDepositToMultiVault(provider, payloadResponse);
console.log('Transfer ID:', txHash);

После успешной транзакции

  1. Транзакция депозита подтверждена в EVM сети
  2. Transfer ID = Transaction Hash этой транзакции
  3. Relay-ноды обнаруживают событие и начинают подтверждение
  4. В credit режиме: Gas Credit Backend автоматически деплоит Event-контракт
  5. В non-credit режиме: пользователь должен сам задеплоить Event (Шаг 4)

Зачем нужен Transfer ID

  • Отслеживание статуса трансфера через Bridge Aggregator API
  • Сборка EventVoteData из EVM receipt для non-credit режима (Шаг 4)
  • Диагностика проблем с трансфером

Шаг 3: Отслеживание статуса трансфера

Цель: Получить статус трансфера и проверить завершение

Ограничение для EVM→TVM

Статус трансфера доступен через API только после того, как трансфер будет завершён (зарелижен) в TVM сети. До этого момента API не возвращает данных по данному трансферу. Для non-credit режима это означает, что проверять статус имеет смысл после выполнения Шага 4 (деплой Event-контракта).

API Endpoint

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

Параметры запроса

typescript
interface StatusRequest {
  evmTvm: {
    transactionHashEvm: string;    // Hash EVM транзакции из Шага 2
    dappChainId: number;           // TVM chain ID сети назначения
    timestampCreatedFrom?: number; // Фильтр по времени (опционально)
  };
}
ПараметрОписание
transactionHashEvmHash EVM транзакции из Шага 2 (Transfer ID)
dappChainIdTVM chain ID сети назначения
timestampCreatedFromМинимальный timestamp для фильтра (опционально)

Пример запроса

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

Пример ответа (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
}

Статусы трансфера

СтатусОписание
PendingТрансфер в процессе обработки. Event-контракт ещё не получил достаточно подтверждений от relay-нод.
CompletedТрансфер успешно завершён. Токены получены в TVM сети.
FailedТрансфер не удался. Требуется изучение логов и повторная попытка.

Шаг 4: Завершение трансфера (Non-Credit режим)

Цель: Задеплоить Event-контракт в TVM (только если useCredit=false)

Когда требуется этот шаг?

useCreditДействие
trueGas Credit Backend автоматически деплоит Event-контракт. Пропустите Шаг 4.
falseВы вручную собираете данные и деплоите Event-контракт. Выполните Шаг 4.

Как это работает

В non-credit режиме данные для деплоя Event-контракта собираются напрямую из блокчейна, а не через API. Процесс:

  1. Получить адрес EventConfiguration из конфигураций моста
  2. Прочитать EVM receipt для извлечения event-логов
  3. Конвертировать EVM данные в TVM формат
  4. Отправить транзакцию deployEvent в TVM

Шаг 4.1: Получение адреса EventConfiguration

Адрес EventConfiguration определяется через endpoint конфигураций моста:

GET {BASE_URL}/payload/configurations/all

Из списка конфигураций выберите нужную по параметрам:

ПараметрЗначениеОписание
chainIdID исходной EVM сетиДолжен совпадать с fromChainId
meta.tokenTypeAlien или NativeТип токена из tokensMeta.sourceTokenType (Шаг 1)
meta.directionToTvmНаправление EVM→TVM
typescript
interface BridgeConfiguration {
  address: string;              // Адрес EventConfiguration в TVM
  chainId: number;              // EVM chain ID
  flags: string;                // Флаги конвертации
  eventInitialBalance: string;  // Минимальный газ для деплоя Event (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(`Конфигурация не найдена для chainId=${fromChainId}, tokenType=${tokenType}`);
  }

  return config;
}

Необходимые ABI

Для Шагов 4.2–4.3 нужны два ABI:

  • EVM-события MultiVault
  • TVM-контракт EventConfiguration
typescript
// 1. ABI событий MultiVault (EVM)
// Источник: 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. ABI контракта EvmTvmEventConfiguration (TVM) — минимальный набор: getDetails + deployEvent
// Источник: 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;

Шаг 4.2: Сборка EventVoteData из EVM receipt

После подтверждения EVM транзакции (Шаг 2) нужно прочитать receipt и извлечь event-логи.

typescript
// Общий интерфейс для обоих вариантов
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 — см. «Необходимые ABI» выше

async function buildEventVoteData(
  evmProvider: ethers.BrowserProvider,
  txHash: string, // Transfer ID из Шага 2
  configurationFlags: string // flags из /payload/configurations/all
): Promise<EventVoteData> {
  // 1. Получить EVM receipt
  const receipt = await evmProvider.getTransactionReceipt(txHash);
  if (!receipt) throw new Error('Receipt транзакции не найден');

  // 2. Найти AlienTransfer или NativeTransfer в логах
  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 { /* не наш лог, пропускаем */ }
  }

  if (!transferEvent || logIndex === undefined || !rawLogData) {
    throw new Error('Событие AlienTransfer или NativeTransfer не найдено в receipt');
  }

  // 3. Конвертировать EVM event data в TVM cell
  // ABI берётся из EVM контракта MultiVault (описание полей события)
  const eventABI = transferEvent.name === 'AlienTransfer'
    ? MULTIVAULT_EVENTS_ABI[0]
    : MULTIVAULT_EVENTS_ABI[1];

  const eventData = mapEthBytesIntoTonCell(eventABI, rawLogData, configurationFlags);

  // 4. Собрать 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 — см. «Необходимые ABI» выше

async function buildEventVoteData(
  evmProvider: ethers.BrowserProvider,
  txHash: string, // Transfer ID из Шага 2
  configurationAddress: string,  // address из Шага 4.1
  configurationFlags: string,    // flags из Шага 4.1
  tvmRpcEndpoint: string // URL JRPC-ноды TVM сети
): Promise<EventVoteData> {
  // 1. Подключиться к TVM и прочитать eventABI из EventConfiguration
  // eventABI — base64-закодированный JSON EVM event ABI, хранится в контракте
  // Пример: {"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. Получить EVM receipt
  const receipt = await evmProvider.getTransactionReceipt(txHash);
  if (!receipt) throw new Error('Receipt транзакции не найден');

  // 3. Найти нужный лог по eventABI из контракта
  // eventABI содержит имя события (AlienTransfer или NativeTransfer) —
  // используем его для парсинга логов, без хардкода ABI
  const iface = new ethers.Interface([JSON.parse(eventABI)]);
  const eventName = JSON.parse(eventABI).name; // 'AlienTransfer' или '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 { /* не наш лог, пропускаем */ }
  }

  if (logIndex === undefined || !rawLogData) {
    throw new Error(`Событие ${eventName} не найдено в receipt`);
  }

  // 4. Конвертировать EVM event data в TVM cell
  // eventABI — из контракта EventConfiguration (пункт 1 выше)
  // configurationFlags — из API /configurations/all (Шаг 4.1)
  const eventData = mapEthBytesIntoTonCell(eventABI, rawLogData, configurationFlags);

  // 5. Собрать EventVoteData
  return {
    eventBlock: receipt.blockHash,
    eventBlockNumber: receipt.blockNumber.toString(),
    eventTransaction: receipt.hash,
    eventIndex: logIndex.toString(),
    eventData,
  };
}

Что используется из EVM receipt

Поле receiptИспользование
blockHasheventVoteData.eventBlock
blockNumbereventVoteData.eventBlockNumber
hash (transactionHash)eventVoteData.eventTransaction — это Transfer ID из Шага 2
logs[i].index (logIndex)eventVoteData.eventIndex
logs[i].dataКонвертируется в TVM cell → eventVoteData.eventData

Шаг 4.3: Деплой Event-контракта

Закодируйте вызов deployEvent и отправьте транзакцию:

typescript
import TonConnectUI from '@tonconnect/ui';
import { beginCell, Cell } from '@ton/core';

// Вычисление function ID по правилам TVM ABI v2:
// SHA-256 от сигнатуры функции, первые 4 байта (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. Вычислить function ID для deployEvent
  // Сигнатура: имя(типы_входных_параметров)(типы_возврата)
  const functionId = await computeTvmFunctionId(
    'deployEvent((uint256,uint32,cell,uint32,uint256))()'
  );

  // 2. Закодировать вызов deployEvent в cell
  // Порядок полей — из 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 хранится как reference
    .storeUint(Number(eventVoteData.eventBlockNumber), 32)
    .storeUint(BigInt(eventVoteData.eventBlock), 256)
    .endCell()
    .toBoc()
    .toString('base64');

  // 3. Рассчитать газ (eventInitialBalance + буфер)
  const expectedNatives = (
    BigInt(eventInitialBalance) + BigInt(500_000_000)
  ).toString();

  // 4. Отправить через TonConnect
  const result = await tonConnectUI.sendTransaction({
    validUntil: Math.floor(Date.now() / 1000) + 600,
    messages: [{
      address: configurationAddress,
      amount: expectedNatives,
      payload,
    }],
  });

  console.log('Event задеплоен, BOC:', result.boc);
  return result.boc;
}
typescript
import { Address, ProviderRpcClient } from 'everscale-inpage-provider';

// EVM_TVM_EVENT_CONFIGURATION_ABI — см. «Необходимые ABI» выше

async function deployEventContract(
  tvmClient: ProviderRpcClient,
  senderAddress: string, // TVM адрес отправителя (кошелёк пользователя)
  configurationAddress: string,
  eventVoteData: EventVoteData,
  eventInitialBalance: string
): Promise<string> {
  const configContract = new tvmClient.Contract(
    EVM_TVM_EVENT_CONFIGURATION_ABI,
    new Address(configurationAddress)
  );

  // 1. Рассчитать газ (eventInitialBalance + буфер)
  const amount = (
    BigInt(eventInitialBalance) + BigInt(500_000_000)
  ).toString();

  // 2. Отправить транзакцию через TVM SDK
  // .send() автоматически вычисляет function ID, кодирует параметры
  // и отправляет транзакцию от имени подключённого кошелька
  const { transaction } = await configContract.methods
    .deployEvent({ _eventVoteData: eventVoteData })
    .send({
      from: new Address(senderAddress),
      amount,
      bounce: true,
    });

  console.log('Event задеплоен, tx hash:', transaction.id.hash);
  return transaction.id.hash;
}

Переиспользование TVM-клиента

Если вы использовали вариант [TVM SDK] в Шаге 4.2, tvmClient уже создан — передайте его в deployEventContract(), не создавая повторно.

Жизненный цикл Event-контракта

После деплоя Event-контракта происходит следующее:

  1. Event-контракт инициализируется с данными трансфера (адрес токена, сумма, получатель)
  2. Event запрашивает конфигурацию у EvmTvmEventConfiguration и публичные ключи relay-нод
  3. Relay-ноды подтверждают событие — каждая relay вызывает confirm() и записывает голос
  4. Достигнут консенсус — когда confirms >= requiredVotes (где requiredVotes = keys.length * 2/3 + 1), Event переходит в Confirmed
  5. Event вызывает callback в ProxyMultiVaultAlien/ProxyMultiVaultNative
  6. Proxy финализирует трансфер — минтит/разлокивает токены пользователю
  7. Трансфер завершён — статус становится Completed

Поиск трансферов

API Endpoint

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

Пример запроса

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
  }'

Параметры фильтрации

ПараметрТипОбязательныйОписание
transferKindsstring[]["EvmToTvm"] для EVM→TVM трансферов
evmUserAddressstringФильтр по EVM адресу отправителя
tvmUserAddressstringФильтр по TVM адресу получателя
statusesstring[]Pending, Completed, Failed
fromEvmChainIdnumberФильтр по исходной EVM сети
toTvmChainIdnumberФильтр по целевой TVM сети
limitnumberЛимит записей
offsetnumberСмещение
isNeedTotalCountbooleanВозвращать ли общее количество

Режимы трансфера: Credit vs Non-Credit

АспектCredit (useCredit=true)Non-Credit (useCredit=false)
Газ в TVMОплачивает Gas Credit BackendОплачивает пользователь
АвтоматизацияПолностью автоматическийТребует ручной деплой Event
СкоростьБыстрее (автоматический деплой)Зависит от пользователя
КомиссияВключена в feesMeta.amountТолько комиссия моста
РекомендацияДля обычных пользователейДля продвинутых/автоматизации

Credit Mode (useCredit=true)

  • Gas Credit Backend автоматически деплоит Event-контракт в TVM
  • Газ платится из комиссии вашего трансфера
  • Пользователю не требуется вмешиваться после депозита в EVM
  • Трансфер завершается автоматически

Non-Credit Mode (useCredit=false)

  • Вы ответственны за деплой Event-контракта
  • Данные для деплоя собираются из EVM receipt + TVM контракта EventConfiguration
  • EVM event-логи конвертируются в TVM формат (eth-ton-abi-converter)
  • Транзакция deployEvent отправляется через TonConnect или TVM SDK
  • Дешевле, но требует дополнительных действий

Полный пример: EVM→TVM трансфер

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}';

// Конфигурация
const CONFIG = {
  evmChainId: 1,
  tvmChainId: -239,
  tokenAddress: '0xdAC1...1ec7', // USDT
  amount: '1000000', // 1 USDT
};

// ABI — см. раздел «Необходимые ABI»:
// MULTIVAULT_EVENTS_ABI — ABI событий MultiVault (EVM)
// EVM_TVM_EVENT_CONFIGURATION_ABI — ABI контракта EvmTvmEventConfiguration (TVM)

// --- Вспомогательные функции (определены в Шагах 2–4) ---

// Шаг 2.1: approveToken(provider, tokenAddress, spenderAddress, amount)
// Шаг 2.2: sendDepositToMultiVault(provider, payloadResponse)
// Шаг 4.1: getConfiguration(fromChainId, tokenType)
// Шаг 4.2: buildEventVoteData(evmProvider, txHash, configurationFlags)
// Шаг 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();

  // === Шаг 1: Build Payload ===
  console.log('[Шаг 1] Сборка 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 собран:', {
    transferKind: payloadResponse.transferKind,
    tokenAmount: payloadResponse.tokenAmount,
    fee: payloadResponse.feesMeta?.amount,
    method: payloadResponse.abiMeta.abiMethod,
  });

  const { abiMeta } = payloadResponse;

  // === Шаг 2: Approve + Deposit ===
  // В примере используется ERC-20 токен (USDT), поэтому abiMethod = "deposit"
  // и approve обязателен. Для native currency (ETH, BNB) approve не нужен.
  if (abiMeta.abiMethod === 'deposit') {
    console.log('\n[Шаг 2.1] Approve ERC-20 токена...');
    await approveToken(
      evmProvider,
      params.tokenAddress,
      abiMeta.executionAddress,
      params.amount
    );
  }

  console.log('\n[Шаг 2.2] Отправка депозита...');
  const txHash = await sendDepositToMultiVault(evmProvider, payloadResponse);
  console.log('Депозит подтверждён. Transfer ID:', txHash);

  // === Шаг 3: Проверка статуса ===
  console.log('\n[Шаг 3] Проверка статуса трансфера...');
  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('Текущий статус:', status);

  // === Шаг 4: Деплой Event (только non-credit) ===
  if (!params.useCredit) {
    console.log('\n[Шаг 4] Деплой Event-контракта (non-credit)...');

    // 4.1: Получить конфигурацию (address, flags, eventInitialBalance)
    const tokenType = payloadResponse.tokensMeta.sourceTokenType; // 'Alien' или 'Native'
    const configuration = await getConfiguration(params.fromChainId, tokenType);

    // 4.2: Собрать EventVoteData из EVM receipt
    const eventVoteData = await buildEventVoteData(
      evmProvider, txHash, configuration.flags
    );

    // 4.3: Деплой Event-контракта через TonConnect
    await deployEventContract(
      tonConnectUI,
      configuration.address,
      eventVoteData,
      configuration.eventInitialBalance
    );
    console.log('Event задеплоен успешно');
  } else {
    console.log('\n[Шаг 4] Пропущен — Credit Backend деплоит Event автоматически');
  }

  console.log('\nТрансфер завершён!');
  return statusResponse;
}

// --- Использование ---
const evmProvider = new ethers.BrowserProvider(window.ethereum);
// TonConnect manifest — JSON-файл с описанием вашего dApp (название, иконка, URL).
// Хостится на вашем домене. Формат: { "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 для non-credit (потребует Шаг 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'; // JRPC-нода TVM сети

// Конфигурация
const CONFIG = {
  evmChainId: 1,
  tvmChainId: -239,
  tokenAddress: '0xdAC1...1ec7', // USDT
  amount: '1000000', // 1 USDT
};

// ABI — см. раздел «Необходимые ABI»:
// MULTIVAULT_EVENTS_ABI — ABI событий MultiVault (EVM)
// EVM_TVM_EVENT_CONFIGURATION_ABI — ABI контракта EvmTvmEventConfiguration (TVM)

// --- Вспомогательные функции (определены в Шагах 2–4) ---

// Шаг 2.1: approveToken(provider, tokenAddress, spenderAddress, amount)
// Шаг 2.2: sendDepositToMultiVault(provider, payloadResponse)
// Шаг 4.1: getConfiguration(fromChainId, tokenType)
// Шаг 4.2: buildEventVoteData(evmProvider, txHash, configurationAddress, configurationFlags, tvmRpcEndpoint)
// Шаг 4.3: deployEventContract(tvmClient, senderAddress, configurationAddress, eventVoteData, eventInitialBalance)

async function evmToTvmTransfer(
  evmProvider: ethers.BrowserProvider,
  tvmSenderAddress: string, // TVM адрес кошелька пользователя
  params: {
    fromChainId: number;
    toChainId: number;
    tokenAddress: string;
    recipientAddress: string;
    amount: string;
    useCredit: boolean;
  }
) {
  const signer = await evmProvider.getSigner();
  const senderAddress = await signer.getAddress();

  // === Шаг 1: Build Payload ===
  console.log('[Шаг 1] Сборка 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 собран:', {
    transferKind: payloadResponse.transferKind,
    tokenAmount: payloadResponse.tokenAmount,
    fee: payloadResponse.feesMeta?.amount,
    method: payloadResponse.abiMeta.abiMethod,
  });

  const { abiMeta } = payloadResponse;

  // === Шаг 2: Approve + Deposit ===
  // В примере используется ERC-20 токен (USDT), поэтому abiMethod = "deposit"
  // и approve обязателен. Для native currency (ETH, BNB) approve не нужен.
  if (abiMeta.abiMethod === 'deposit') {
    console.log('\n[Шаг 2.1] Approve ERC-20 токена...');
    await approveToken(
      evmProvider,
      params.tokenAddress,
      abiMeta.executionAddress,
      params.amount
    );
  }

  console.log('\n[Шаг 2.2] Отправка депозита...');
  const txHash = await sendDepositToMultiVault(evmProvider, payloadResponse);
  console.log('Депозит подтверждён. Transfer ID:', txHash);

  // === Шаг 3: Проверка статуса ===
  console.log('\n[Шаг 3] Проверка статуса трансфера...');
  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('Текущий статус:', status);

  // === Шаг 4: Деплой Event (только non-credit) ===
  if (!params.useCredit) {
    console.log('\n[Шаг 4] Деплой Event-контракта (non-credit)...');

    // 4.1: Получить конфигурацию (address, flags, eventInitialBalance)
    const tokenType = payloadResponse.tokensMeta.sourceTokenType; // 'Alien' или 'Native'
    const configuration = await getConfiguration(params.fromChainId, tokenType);

    // Создать TVM-клиент (переиспользуется в Шагах 4.2 и 4.3)
    const tvmClient = new ProviderRpcClient({
      fallback: () => EverscaleStandaloneClient.create({
        connection: { type: 'jrpc', data: { endpoint: TVM_RPC_ENDPOINT } },
      }),
    });
    await tvmClient.ensureInitialized();

    // 4.2: Собрать EventVoteData из EVM receipt (eventABI читается из контракта)
    const eventVoteData = await buildEventVoteData(
      evmProvider, txHash, configuration.address, configuration.flags, TVM_RPC_ENDPOINT
    );

    // 4.3: Деплой Event-контракта через TVM SDK
    await deployEventContract(
      tvmClient,
      tvmSenderAddress,
      configuration.address,
      eventVoteData,
      configuration.eventInitialBalance
    );
    console.log('Event задеплоен успешно');
  } else {
    console.log('\n[Шаг 4] Пропущен — Credit Backend деплоит Event автоматически');
  }

  console.log('\nТрансфер завершён!');
  return statusResponse;
}

// --- Использование ---
const evmProvider = new ethers.BrowserProvider(window.ethereum);
const tvmSenderAddress = '0:1111...1111'; // TVM адрес подключённого кошелька

await evmToTvmTransfer(evmProvider, tvmSenderAddress, {
  fromChainId: CONFIG.evmChainId,
  toChainId: CONFIG.tvmChainId,
  tokenAddress: CONFIG.tokenAddress,
  recipientAddress: '0:1111...1111',
  amount: CONFIG.amount,
  useCredit: true, // false для non-credit (потребует Шаг 4)
});

Справка по ошибкам

EVM ошибки при Deposit

ОшибкаПричинаРешение
Msg value to lowmsg.value слишком мала для нативной валютыУвеличьте abiMeta.attachedValue
Deposit: limits violatedЛимит на сумму депозита превышенДождитесь сброса лимитов (скользящее окно 24ч) или уменьшите сумму
Deposit amount too is largeСумма >= 2^128Отправьте несколько меньших трансферов
Pending: already filledПопытка заполнить закрытый pendingИспользуйте другой pending withdrawal
Pending: wrong tokenToken не совпадает с ожидаемымПроверьте tokenAddress в запросе
Pending: deposit insufficientСумма недостаточна для покрытия pendingУвеличьте amount в запросе
Emergency: shutdownМост в режиме emergency shutdownДождитесь деактивации режима
Tokens: token is blacklistedТокен в blacklist (для ERC-20)Свяжитесь с командой, токен заблокирован
Tokens: weth is blacklistedWETH в blacklist (для native currency)Свяжитесь с командой
Tokens: invalid token metaНекорректные метаданные токена (decimals/symbol/name)Свяжитесь с командой
Insufficient allowanceНедостаточный approveВыполните approve на нужную сумму
ReentrancyGuard: reentrant callПопытка реентрантного вызоваСистемная ошибка, свяжитесь с командой

TVM ошибки при Event Deploy (Non-Credit)

Ошибка (код)ПричинаРешение
2213msg.value < eventInitialBalanceУвеличьте сумму газа (используйте eventInitialBalance из конфигурации + буфер)
2105Event configuration не зарегистрированаСистемная ошибка, свяжитесь с командой
2103Event configuration деактивированаСистемная ошибка, свяжитесь с командой

ChainConnect Bridge Documentation