Skip to content
ChainConnect

Отправка трансфера

Шаг 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

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

typescript
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 трансферов) 
  };
}
ПараметрТипОбязательныйОписание
fromChainIdnumberChain ID сети отправления
toChainIdnumberChain ID сети назначения
tokenAddressstringАдрес токена в сети отправления (формат 0:...)
recipientAddressstringАдрес получателя в сети назначения
amountstringСумма в nano-единицах (integer string, напр. "1000000" для 1 USDT с 6 decimals)
senderAddressstringАдрес отправителя в сети отправления
useCreditbooleantrue = Credit Backend оплачивает газ в сети назначения. Default: false
remainingGasTostringКуда вернуть остаток газа. Игнорируется если useCredit=true
tokenBalance.nativeTokenAmountstring⚠️Баланс газового токена (native currency). Обязателен при отправке газового токена
tokenBalance.wrappedNativeTokenAmountstring⚠️Баланс wrapped версии газового токена (например, wTON). Обязателен при отправке газового токена

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

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

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

json
{
  "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.txPayload транзакции (BOC base64)Шаг 2
abiMeta.attachedValueСумма для прикрепления к TX (nanoTON)Шаг 2
trackingMeta.sourceProxyАдрес proxy контрактаДля отслеживания трансфера (Шаг 3)

Шаг 2: Отправка транзакции в блокчейн

Цель: Выполнить транзакцию и получить Transaction Hash

Параметры транзакции

ПараметрЗначениеИсточник
recipientabiMeta.executionAddressИз ответа Шага 1
amountabiMeta.attachedValueИз ответа Шага 1
payloadabiMeta.txИз ответа Шага 1
bouncetrueВсегда

Пример на TypeScript (TonConnect UI)

Документация

TON Connect for JS, Sending Messages

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

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

  1. Транзакция от кошелька запускает цепочку внутренних вызовов в блокчейне
  2. В этой цепочке Proxy контракт (sourceProxy) эмитит событие TvmTvmNative или TvmTvmAlien
  3. 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 эмитит событие.

После отправки транзакции нужно:

  1. Дождаться подтверждения транзакции в блокчейне
  2. Получить trace цепочки внутренних транзакций
  3. Найти транзакцию на Proxy контракте с External Out Message (событие)
  4. Hash этой транзакции = Transfer ID

Пример (TypeScript)

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:

typescript
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Действие
trueCredit Backend автоматически деплоит event. Пропустите этот шаг.
falseНужно вручную получить proof и задеплоить event контракт в сети назначения.

API Endpoint

POST https://tetra-history-api.chainconnect.com/v2/transfers/status
Content-Type: application/json

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

bash
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

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

  1. Получить proofPayload из ответа API
  2. Отправить транзакцию на EventConfiguration.deployEvent() в сети назначения
  3. Event контракт верифицирует транзакцию через TransactionCheckerLiteClient
  4. После успешной верификации proxy в сети назначения mint'ит/unlock'ает токены

Отправка транзакции для non-credit

Требуемый газ

Для деплоя Event контракта необходимо приложить ~3-5 TON (или эквивалент в нативной валюте сети назначения). Рекомендуемое значение: 5 TON для гарантированного выполнения.

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

ChainConnect Bridge Documentation