Отправка трансфера
Шаг 1: Подготовка трансфера
Цель: Получить payload для отправки транзакции в блокчейн
API Endpoint
POST https://tetra-history-api.chainconnect.com/v2/payload/build
Content-Type: application/jsonТестовое окружение
Для тестирования используйтеhttps://history-api-test.chainconnect.com/v2/payload/build
Параметры запроса
interface TransferPayloadRequest {
fromChainId: number; // Chain ID сети отправления
toChainId: number; // Chain ID сети назначения
tokenAddress: string; // Адрес токена в сети отправления (формат `0:...`)
recipientAddress: string; // Адрес получателя в сети назначения
amount: string; // Сумма в nano-единицах (integer string)
senderAddress: string; // Адрес отправителя в сети отправления
useCredit?: boolean; // `true` = Credit Backend оплачивает газ в сети назначения. Default: `false`
remainingGasTo?: string; // Куда вернуть остаток газа. Игнорируется если `useCredit=true`
payload?: string; // Дополнительный payload
evmChainId?: number; // EVM chain ID (для EVM трансферов)
callback?: CallbackRequest; // Callback
tokenBalance?: { // Для native currency (wrapped native token)
nativeTokenAmount: string; // Баланс native currency (для wrapped native token трансферов)
wrappedNativeTokenAmount: string; // Баланс wrapped native (для wrapped native token трансферов)
};
}| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
fromChainId | number | ✅ | Chain ID сети отправления |
toChainId | number | ✅ | Chain ID сети назначения |
tokenAddress | string | ✅ | Адрес токена в сети отправления (формат 0:...) |
recipientAddress | string | ✅ | Адрес получателя в сети назначения |
amount | string | ✅ | Сумма в nano-единицах (integer string, напр. "1000000" для 1 USDT с 6 decimals) |
senderAddress | string | ✅ | Адрес отправителя в сети отправления |
useCredit | boolean | ❌ | true = Credit Backend оплачивает газ в сети назначения. Default: false |
remainingGasTo | string | ❌ | Куда вернуть остаток газа. Игнорируется если useCredit=true |
tokenBalance.nativeTokenAmount | string | ⚠️ | Баланс газового токена (native currency). Обязателен при отправке газового токена |
tokenBalance.wrappedNativeTokenAmount | string | ⚠️ | Баланс wrapped версии газового токена (например, wTON). Обязателен при отправке газового токена |
Пример запроса
curl -X POST 'https://tetra-history-api.chainconnect.com/v2/payload/build' \
-H 'Content-Type: application/json' \
-d '{
"fromChainId": -239,
"toChainId": 2000,
"tokenAddress": "0:1111...aaaa",
"recipientAddress": "0:2222...bbbb",
"amount": "1000000",
"senderAddress": "0:3333...cccc",
"useCredit": true,
"remainingGasTo": "0:3333...cccc"
}'Пример ответа
{
"transferKind": "TvmToTvm",
"tokensMeta": {
"targetToken": {
"tokenType": "Alien",
"isDeployed": true,
"address": "0:4444...dddd"
},
"sourceTokenType": "Native"
},
"tokenAmount": "999000",
"feeAmount": "1000",
"gasEstimateAmount": "155113636",
"abiMeta": {
"tx": "te6ccgEBBwEA6AABiw/X8c8...",
"executionAddress": "0:5555...eeee",
"attachedValue": "155113636"
},
"trackingMeta": {
"sourceProxy": "0:6666...ffff",
"targetProxy": "0:7777...0000",
"targetConfiguration": "0:8888...1111"
},
"payload": null
}Что потребуется дальше
| Поле | Описание | Использование |
|---|---|---|
abiMeta.executionAddress | Адрес для отправки транзакции | Шаг 2 |
abiMeta.tx | Payload транзакции (BOC base64) | Шаг 2 |
abiMeta.attachedValue | Сумма для прикрепления к TX (nanoTON) | Шаг 2 |
trackingMeta.sourceProxy | Адрес proxy контракта | Для отслеживания трансфера (Шаг 3) |
Шаг 2: Отправка транзакции в блокчейн
Цель: Выполнить транзакцию и получить Transaction Hash
Параметры транзакции
| Параметр | Значение | Источник |
|---|---|---|
recipient | abiMeta.executionAddress | Из ответа Шага 1 |
amount | abiMeta.attachedValue | Из ответа Шага 1 |
payload | abiMeta.tx | Из ответа Шага 1 |
bounce | true | Всегда |
Пример на TypeScript (TonConnect UI)
Документация
import TonConnectUI from '@tonconnect/ui';
import { toNano } from '@ton/ton';
// Инициализация TonConnect UI
const tonConnectUI = new TonConnectUI({
manifestUrl: 'https://your-app.com/tonconnect-manifest.json',
});
async function sendTransfer(
payloadResponse: TransferPayloadResponse
): Promise<string> {
const { abiMeta } = payloadResponse;
// Формируем транзакцию
const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 300, // 5 минут
messages: [
{
address: abiMeta.executionAddress, // Адрес контракта
amount: abiMeta.attachedValue, // Газ в nanoTON
payload: abiMeta.tx, // BOC base64 из API
},
],
};
// Отправляем транзакцию через TonConnect
const result = await tonConnectUI.sendTransaction(transaction);
console.log('Transaction BOC:', result.boc);
return result.boc;
}После успешной транзакции
- Транзакция от кошелька запускает цепочку внутренних вызовов в блокчейне
- В этой цепочке Proxy контракт (
sourceProxy) эмитит событиеTvmTvmNativeилиTvmTvmAlien - Transfer ID = transaction hash той транзакции в цепочке, в которой эмитится событие (не hash исходной транзакции от кошелька)
Шаг 3: Получение Transfer ID
Зачем нужен Transfer ID
Transfer ID необходим для:
- Отслеживания статуса трансфера через Bridge Aggregator API
- Получения proof для non-credit режима
- Диагностики проблем с трансфером
Transfer ID = transaction hash транзакции с событием TvmTvmNative или TvmTvmAlien на Proxy контракте.
Transaction на Proxy ← Transaction Hash = TRANSFER ID
└── Out Messages
└── External Out Message (событие TvmTvmNative/TvmTvmAlien)Как получить Transfer ID
Важно
result.boc из TON Connect — это BOC подписанного сообщения от кошелька, а не Transfer ID. Transfer ID появляется только после того, как транзакция обработана в блокчейне и Proxy эмитит событие.
После отправки транзакции нужно:
- Дождаться подтверждения транзакции в блокчейне
- Получить trace цепочки внутренних транзакций
- Найти транзакцию на Proxy контракте с External Out Message (событие)
- Hash этой транзакции = Transfer ID
Пример (TypeScript)
// npm install @ton/ton @ton/core @ton/crypto
import { Cell, beginCell, Address, TonClient, Transaction } from '@ton/ton';
import { sha256 } from '@ton/crypto';
// === КОНФИГУРАЦИЯ ===
const TONCENTER_API = 'https://toncenter.com/api/v2'; // Mainnet
// const TONCENTER_API = 'https://testnet.toncenter.com/api/v2'; // Testnet
/**
* Парсит BOC от TonConnect и возвращает hash внешнего сообщения
*/
function getExternalMessageHash(bocBase64: string): string {
const cell = Cell.fromBase64(bocBase64);
return cell.hash().toString('hex');
}
/**
* Ожидает появления транзакции в блокчейне
*/
async function waitForTransaction(
walletAddress: string,
messageHash: string,
maxAttempts = 30,
delayMs = 3000
): Promise<Transaction | null> {
const address = Address.parse(walletAddress);
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fetch(
`${TONCENTER_API}/getTransactions?` +
`address=${address.toString()}&limit=10&archival=true`
);
const data = await response.json();
if (!data.ok || !data.result) {
await new Promise(r => setTimeout(r, delayMs));
continue;
}
// Ищем транзакцию с нашим входящим сообщением
for (const tx of data.result) {
if (tx.in_msg?.hash === messageHash) {
console.log(`Transaction found at attempt ${attempt + 1}`);
return tx;
}
}
console.log(`Attempt ${attempt + 1}: waiting for transaction...`);
await new Promise(r => setTimeout(r, delayMs));
}
return null;
}
/**
* Получает trace транзакции (все внутренние транзакции в цепочке)
*/
async function getTransactionTrace(
txHash: string,
txLt: string
): Promise<any[]> {
// Используем TonAPI для получения trace (более удобный API для этого)
const response = await fetch(
`https://tonapi.io/v2/traces/${txHash}`,
{
headers: {
'Accept': 'application/json',
}
}
);
if (!response.ok) {
throw new Error(`Failed to get trace: ${response.status}`);
}
const trace = await response.json();
return flattenTrace(trace);
}
/**
* Рекурсивно извлекает все транзакции из trace
*/
function flattenTrace(node: any, result: any[] = []): any[] {
if (node.transaction) {
result.push(node.transaction);
}
if (node.children) {
for (const child of node.children) {
flattenTrace(child, result);
}
}
return result;
}
/**
* Ищет транзакцию на Proxy контракте с событием TvmTvmNative/TvmTvmAlien
*/
function findProxyTransaction(
transactions: any[],
proxyAddress: string
): { hash: string; lt: string } | null {
const normalizedProxy = Address.parse(proxyAddress).toString();
for (const tx of transactions) {
const txAccount = tx.account?.address;
if (!txAccount) continue;
// Проверяем что транзакция на Proxy контракте
const txAddress = Address.parseRaw(txAccount).toString();
if (txAddress !== normalizedProxy) continue;
// Проверяем наличие External Out Message (событие)
const outMsgs = tx.out_msgs || [];
const hasExternalOut = outMsgs.some(
(msg: any) => msg.destination === null || msg.destination === ''
);
if (hasExternalOut) {
return {
hash: tx.hash,
lt: tx.lt,
};
}
}
return null;
}
/**
* Главная функция: получает Transfer ID из BOC
*/
async function getTransferIdFromBoc(
bocBase64: string,
walletAddress: string,
proxyAddress: string
): Promise<string> {
console.log('=== Step 1: Parsing BOC ===');
const messageHash = getExternalMessageHash(bocBase64);
console.log('External message hash:', messageHash);
console.log('\n=== Step 2: Waiting for transaction ===');
const walletTx = await waitForTransaction(walletAddress, messageHash);
if (!walletTx) {
throw new Error('Transaction not found in blockchain');
}
console.log('Wallet transaction hash:', walletTx.transaction_id.hash);
console.log('Wallet transaction lt:', walletTx.transaction_id.lt);
console.log('\n=== Step 3: Getting transaction trace ===');
const trace = await getTransactionTrace(
walletTx.transaction_id.hash,
walletTx.transaction_id.lt
);
console.log(`Found ${trace.length} transactions in trace`);
console.log('\n=== Step 4: Finding Proxy transaction ===');
const proxyTx = findProxyTransaction(trace, proxyAddress);
if (!proxyTx) {
throw new Error('Proxy transaction not found in trace');
}
console.log('\n=== Result ===');
console.log('Transfer ID:', proxyTx.hash);
return proxyTx.hash;
}
// === ПРИМЕР ИСПОЛЬЗОВАНИЯ ===
async function example() {
// После отправки транзакции через TonConnect:
// const result = await tonConnectUI.sendTransaction(transaction);
// const bocBase64 = result.boc;
const bocBase64 = 'te6cckEBAgEA...'; // BOC от TonConnect
const walletAddress = '0:3333...cccc'; // Адрес кошелька отправителя
const proxyAddress = '0:6666...ffff'; // Из payload response (trackingMeta.sourceProxy)
const transferId = await getTransferIdFromBoc(bocBase64, walletAddress, proxyAddress);
console.log('Transfer ID:', transferId);
}
// example().catch(console.error);Альтернатива: через TonClient (standalone)
Если вы используете standalone клиент без TonConnect, можно получить Transfer ID напрямую через TonClient:
import { TonClient, Address } from '@ton/ton';
async function getTransferIdFromWalletTx(
client: TonClient,
walletTxHash: string,
proxyAddress: string
): Promise<string | null> {
// Получаем trace через TonAPI
const response = await fetch(`https://tonapi.io/v2/traces/${walletTxHash}`);
const trace = await response.json();
const normalizedProxy = Address.parse(proxyAddress).toRawString().toLowerCase();
// Рекурсивно ищем транзакцию на Proxy
function findInTrace(node: any): string | null {
const tx = node.transaction;
if (tx?.account?.address) {
const txAddress = tx.account.address.toLowerCase();
const isProxy = txAddress.includes(normalizedProxy.slice(2));
// Проверяем External Out Message (событие)
const hasEvent = tx.out_msgs?.some(
(m: any) => !m.destination || m.destination?.address === ''
);
if (isProxy && hasEvent) {
return tx.hash;
}
}
for (const child of node.children || []) {
const result = findInTrace(child);
if (result) return result;
}
return null;
}
return findInTrace(trace);
}
// Использование
const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' });
const transferId = await getTransferIdFromWalletTx(
client,
'abc123...', // Hash транзакции кошелька
'0:6666...ffff' // proxyAddress из trackingMeta.sourceProxy
);Шаг 4: Запрос Proof через Bridge Aggregator API
Цель: Получить данные для завершения трансфера (если useCredit=false)
Условия
useCredit | Действие |
|---|---|
true | Credit Backend автоматически деплоит event. Пропустите этот шаг. |
false | Нужно вручную получить proof и задеплоить event контракт в сети назначения. |
API Endpoint
POST https://tetra-history-api.chainconnect.com/v2/transfers/status
Content-Type: application/jsonПример запроса
curl -X POST 'https://tetra-history-api.chainconnect.com/v2/transfers/status' \
-H 'Content-Type: application/json' \
-d '{
"tvmTvm": {
"outgoingTransactionHash": "abc123def456...",
"dappChainId": -239,
"timestampCreatedFrom": null
}
}'Пример ответа с Proof
{
"transfer": {
"tvmTvm": {
"transferStatus": "Pending",
"timestampCreatedAt": 1767972717,
"outgoing": {
"tokenType": "Native",
"contractAddress": "0:aaaa...1111",
"chainId": -239,
"userAddress": "0:3333...cccc",
"tokenAddress": "0:1111...aaaa",
"proxyAddress": "0:6666...ffff",
"volumeExec": "0.1000",
"messageHash": "abc123def456...",
"transactionHash": "def789abc012..."
},
"incoming": {
"tokenType": null,
"chainId": 2000,
"userAddress": "0:2222...bbbb"
}
}
},
"notInstantTransfer": null,
"proofPayload": {
"txBlockProof": "te6ccgECCg...",
"txProof": "te6ccgEBBw...",
"messageHash": "abc123def456...",
"outMessageIndex": 0,
"event": {
"tokenType": "Native",
"chainId": 2000,
"token": "0:1111...aaaa",
"amount": "99000",
"recipient": "0:2222...bbbb",
"value": "150000000",
"expectedGas": "0",
"remainingGasTo": "0:3333...cccc",
"sender": "0:3333...cccc",
"payload": "te6ccgEBAQEAAgAAAA==",
"nativeProxyWallet": "0:9999...2222",
"name": "Tether USD",
"symbol": "USD₮",
"decimals": 6
},
"feeAmount": "1000"
}
}Что делать с Proof (для non-credit)
- Получить
proofPayloadиз ответа API - Отправить транзакцию на
EventConfiguration.deployEvent()в сети назначения - Event контракт верифицирует транзакцию через
TransactionChecker→LiteClient - После успешной верификации proxy в сети назначения mint'ит/unlock'ает токены
Отправка транзакции для non-credit
Требуемый газ
Для деплоя Event контракта необходимо приложить ~3-5 TON (или эквивалент в нативной валюте сети назначения). Рекомендуемое значение: 5 TON для гарантированного выполнения.
import { Address, toNano, beginCell } from '@ton/ton';
async function deployEventContract(
client: TonClient,
eventConfigurationAddress: string, // Адрес EventConfiguration в сети назначения
proofPayload: ProofPayload,
senderWallet: any
): Promise<void> {
const eventConfig = Address.parse(eventConfigurationAddress);
// Формируем payload для deployEvent
// Структура зависит от конкретной реализации контракта
const deployPayload = beginCell()
.storeRef(Cell.fromBase64(proofPayload.txBlockProof)) // proof блока
.storeRef(Cell.fromBase64(proofPayload.txProof)) // proof транзакции
.storeUint(proofPayload.outMessageIndex, 16) // индекс сообщения
.endCell();
// Отправляем транзакцию с достаточным газом
await senderWallet.sendTransfer({
to: eventConfig,
value: toNano('5'), // 5 TON для газа
body: deployPayload,
});
console.log('deployEvent transaction sent');
}
// Использование
const { proofPayload } = await getTransferStatus(transferId, chainId);
if (proofPayload) {
await deployEventContract(
client,
'0:8888...1111', // EventConfiguration address (из trackingMeta.targetConfiguration)
proofPayload,
wallet
);
}Адрес EventConfiguration
Адрес EventConfiguration в сети назначения можно получить из поля trackingMeta.targetConfiguration в ответе /v2/payload/build (Шаг 1).