Трансфер EVM→TVM
Обзор
Трансфер EVM→TVM позволяет отправить токены из EVM сети в TVM сеть. Процесс включает:
- Депозит в MultiVault — пользователь отправляет токены в EVM контракт (lock для Alien, burn для Native)
- Индексация события — relay-ноды и индексеры отслеживают депозит
- Деплой Event-контракта — relay-нода создаёт Event-контракт в TVM сети
- Консенсус relay-нод — relay-ноды вызывают
confirm()(без подписей, только голосование) - Callback в Proxy — при достаточном количестве голосов Event вызывает callback в Proxy-контракт
- Финальное действие — 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 | Описание |
|---|---|---|---|
| Alien | lock в MultiVault | mint alien | Токен из EVM блокируется, в TVM создаётся alien-представление |
| Native | burn в MultiVault | unlock native | Токен возвращается из EVM, в TVM разблокируется |
API Base URL
Все API-запросы в этом гайде используют один из двух окружений:
| Окружение | Base URL |
|---|---|
| Production | https://tetra-history-api.chainconnect.com/v2 |
| Testnet | https://history-api-test.chainconnect.com/v2 |
Эндпоинты, используемые в гайде:
| Эндпоинт | Метод | Шаг |
|---|---|---|
/payload/build | POST | Шаг 1 — сборка payload |
/transfers/status | POST | Шаг 3 — проверка статуса |
/payload/configurations/all | GET | Шаг 4.1 — конфигурации моста |
/transfers/search | POST | Поиск трансферов |
Шаг 1: Подготовка трансфера (Build Payload)
Цель: Получить payload для отправки транзакции в EVM сеть
API Endpoint
POST {BASE_URL}/payload/build
Content-Type: application/jsonПараметры запроса
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 трансферов)
};
}| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
fromChainId | number | ✅ | Chain ID EVM сети отправления |
toChainId | number | ✅ | Chain ID TVM сети назначения |
tokenAddress | string | ✅ | Адрес ERC-20 токена в EVM (формат 0x... lowercase) |
recipientAddress | string | ✅ | Адрес получателя в TVM (формат 0:...) |
amount | string | ✅ | Сумма в минимальных единицах (целое число как строка, напр. "1000000" для 1 USDT с 6 decimals) |
senderAddress | string | ✅ | Адрес отправителя в EVM (формат 0x... lowercase) |
useCredit | boolean | ❌ | Если true — Gas Credit Backend автоматически деплоит Event-контракт в TVM и оплачивает газ. Default: false |
remainingGasTo | string | ❌ | Адрес для возврата остатка газа (используется если useCredit=false) |
callback | object | ❌ | Параметры callback (адрес EVM контракта для вызова после завершения трансфера) |
tokenBalance.nativeTokenAmount | string | ⚠️ | Баланс газового токена (native currency). Обязателен при отправке газового токена |
tokenBalance.wrappedNativeTokenAmount | string | ⚠️ | Баланс wrapped версии газового токена. Обязателен при отправке газового токена |
Пример запроса
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
}'Пример ответа
{
"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.tx | Calldata транзакции (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.tx | Calldata транзакции (hex) | Поле data в EVM транзакции |
abiMeta.attachedValue | Количество wei для отправки | Поле value в EVM транзакции |
abiMeta.abiMethod | deposit или depositByNativeToken | Определяет нужен ли approve (Шаг 2.1) |
Методы депозита
| Метод | Когда использовать | msg.value |
|---|---|---|
deposit() | Для ERC-20 токенов (Alien) | Только газ для credit режима |
depositByNativeToken() | Для native currency (ETH, BNB и др.) | Сумма депозита + газ |
Шаг 2.1: Approve токена (для ERC-20)
Перед депозитом ERC-20 токена необходимо разрешить MultiVault списать токены:
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: Отправка депозита
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 токен)
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 и др.)
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);После успешной транзакции
- Транзакция депозита подтверждена в EVM сети
- Transfer ID = Transaction Hash этой транзакции
- Relay-ноды обнаруживают событие и начинают подтверждение
- В credit режиме: Gas Credit Backend автоматически деплоит Event-контракт
- В 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Параметры запроса
interface StatusRequest {
evmTvm: {
transactionHashEvm: string; // Hash EVM транзакции из Шага 2
dappChainId: number; // TVM chain ID сети назначения
timestampCreatedFrom?: number; // Фильтр по времени (опционально)
};
}| Параметр | Описание |
|---|---|
transactionHashEvm | Hash EVM транзакции из Шага 2 (Transfer ID) |
dappChainId | TVM chain ID сети назначения |
timestampCreatedFrom | Минимальный timestamp для фильтра (опционально) |
Пример запроса
curl -X POST '{BASE_URL}/transfers/status' \
-H 'Content-Type: application/json' \
-d '{
"evmTvm": {
"transactionHashEvm": "0x1234...abcd",
"dappChainId": -239
}
}'Пример ответа (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
}Статусы трансфера
| Статус | Описание |
|---|---|
Pending | Трансфер в процессе обработки. Event-контракт ещё не получил достаточно подтверждений от relay-нод. |
Completed | Трансфер успешно завершён. Токены получены в TVM сети. |
Failed | Трансфер не удался. Требуется изучение логов и повторная попытка. |
Шаг 4: Завершение трансфера (Non-Credit режим)
Цель: Задеплоить Event-контракт в TVM (только если useCredit=false)
Когда требуется этот шаг?
useCredit | Действие |
|---|---|
true | Gas Credit Backend автоматически деплоит Event-контракт. Пропустите Шаг 4. |
false | Вы вручную собираете данные и деплоите Event-контракт. Выполните Шаг 4. |
Как это работает
В non-credit режиме данные для деплоя Event-контракта собираются напрямую из блокчейна, а не через API. Процесс:
- Получить адрес EventConfiguration из конфигураций моста
- Прочитать EVM receipt для извлечения event-логов
- Конвертировать EVM данные в TVM формат
- Отправить транзакцию
deployEventв TVM
Шаг 4.1: Получение адреса EventConfiguration
Адрес EventConfiguration определяется через endpoint конфигураций моста:
GET {BASE_URL}/payload/configurations/allИз списка конфигураций выберите нужную по параметрам:
| Параметр | Значение | Описание |
|---|---|---|
chainId | ID исходной EVM сети | Должен совпадать с fromChainId |
meta.tokenType | Alien или Native | Тип токена из tokensMeta.sourceTokenType (Шаг 1) |
meta.direction | ToTvm | Направление EVM→TVM |
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
// 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-логи.
// Общий интерфейс для обоих вариантов
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 — см. «Необходимые 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,
};
}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 | Использование |
|---|---|
blockHash | eventVoteData.eventBlock |
blockNumber | eventVoteData.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 и отправьте транзакцию:
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;
}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-контракта происходит следующее:
- Event-контракт инициализируется с данными трансфера (адрес токена, сумма, получатель)
- Event запрашивает конфигурацию у EvmTvmEventConfiguration и публичные ключи relay-нод
- Relay-ноды подтверждают событие — каждая relay вызывает
confirm()и записывает голос - Достигнут консенсус — когда
confirms >= requiredVotes(гдеrequiredVotes = keys.length * 2/3 + 1), Event переходит в Confirmed - Event вызывает callback в ProxyMultiVaultAlien/ProxyMultiVaultNative
- Proxy финализирует трансфер — минтит/разлокивает токены пользователю
- Трансфер завершён — статус становится
Completed
Поиск трансферов
API Endpoint
POST {BASE_URL}/transfers/search
Content-Type: application/jsonПример запроса
curl -X POST '{BASE_URL}/transfers/search' \
-H 'Content-Type: application/json' \
-d '{
"transferKinds": ["EvmToTvm"],
"evmUserAddress": "0x742d...Ab12",
"limit": 10,
"offset": 0,
"isNeedTotalCount": true
}'Параметры фильтрации
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
transferKinds | string[] | ❌ | ["EvmToTvm"] для EVM→TVM трансферов |
evmUserAddress | string | ❌ | Фильтр по EVM адресу отправителя |
tvmUserAddress | string | ❌ | Фильтр по TVM адресу получателя |
statuses | string[] | ❌ | Pending, Completed, Failed |
fromEvmChainId | number | ❌ | Фильтр по исходной EVM сети |
toTvmChainId | number | ❌ | Фильтр по целевой TVM сети |
limit | number | ✅ | Лимит записей |
offset | number | ✅ | Смещение |
isNeedTotalCount | boolean | ✅ | Возвращать ли общее количество |
Режимы трансфера: 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 трансфер
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)
});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 low | msg.value слишком мала для нативной валюты | Увеличьте abiMeta.attachedValue |
Deposit: limits violated | Лимит на сумму депозита превышен | Дождитесь сброса лимитов (скользящее окно 24ч) или уменьшите сумму |
Deposit amount too is large | Сумма >= 2^128 | Отправьте несколько меньших трансферов |
Pending: already filled | Попытка заполнить закрытый pending | Используйте другой pending withdrawal |
Pending: wrong token | Token не совпадает с ожидаемым | Проверьте tokenAddress в запросе |
Pending: deposit insufficient | Сумма недостаточна для покрытия pending | Увеличьте amount в запросе |
Emergency: shutdown | Мост в режиме emergency shutdown | Дождитесь деактивации режима |
Tokens: token is blacklisted | Токен в blacklist (для ERC-20) | Свяжитесь с командой, токен заблокирован |
Tokens: weth is blacklisted | WETH в blacklist (для native currency) | Свяжитесь с командой |
Tokens: invalid token meta | Некорректные метаданные токена (decimals/symbol/name) | Свяжитесь с командой |
Insufficient allowance | Недостаточный approve | Выполните approve на нужную сумму |
ReentrancyGuard: reentrant call | Попытка реентрантного вызова | Системная ошибка, свяжитесь с командой |
TVM ошибки при Event Deploy (Non-Credit)
| Ошибка (код) | Причина | Решение |
|---|---|---|
| 2213 | msg.value < eventInitialBalance | Увеличьте сумму газа (используйте eventInitialBalance из конфигурации + буфер) |
| 2105 | Event configuration не зарегистрирована | Системная ошибка, свяжитесь с командой |
| 2103 | Event configuration деактивирована | Системная ошибка, свяжитесь с командой |