Отправка транзакций
Отправка транзакций через TON Connect — это основной способ взаимодействия пользователя с блокчейном из вашего dApp. Через TON Connect можно отправлять сложные транзакции: вызовы смарт-контрактов, мультисообщения и даже деплой контрактов. Правильная реализация отправки транзакций критична для безопасности пользовательских средств.
После подключения кошелька через TON Connect dApp может запрашивать у пользователя подписание транзакций. В этом уроке разберём формат запроса, работу с nanoTON, payload для вызова контрактов и обработку ошибок.
sendTransaction
Для отправки транзакции используйте хук useTonConnectUI():
import { useTonConnectUI } from '@tonconnect/ui-react';
function SendButton() {
const [tonConnectUI] = useTonConnectUI();
const handleSend = async () => {
const result = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 300,
messages: [
{
address: 'EQBYivdc0GAk-nnczaMnYNuSjpeXu2nJS3DZ4KqLjosX5sVC',
amount: '100000000',
},
],
});
console.log('BOC:', result.boc);
};
return <button onClick={handleSend}>Отправить 0.1 TON</button>;
}
При вызове sendTransaction SDK отправляет запрос кошельку через bridge. Кошелёк показывает пользователю детали транзакции и просит подтверждение. После подписания кошелёк возвращает подписанный BOC (Bag of Cells).
Формат запроса
interface SendTransactionRequest {
validUntil: number; // Unix timestamp -- срок действия
network?: string; // '-239' (mainnet) или '-3' (testnet)
messages: Message[]; // Массив исходящих сообщений (1-4)
}
interface Message {
address: string; // Адрес получателя
amount: string; // Сумма в nanoTON (строка)
payload?: string; // BOC-encoded тело сообщения (base64)
stateInit?: string; // BOC-encoded stateInit для деплоя (base64)
}
Ключевые поля
validUntil — Unix timestamp, после которого транзакция считается просроченной. Кошелёк откажется подписывать просроченную транзакцию.
Всегда устанавливайте validUntil
Транзакция без ограничения по времени — это угроза безопасности. Если пользователь не подтвердит транзакцию сразу, она может быть подписана позже, когда условия уже изменились (курс, баланс контракта и т.д.). Рекомендуемый срок: 5 минут (300 секунд).
network — идентификатор сети. Если указан, кошелёк проверит, что подключён к правильной сети. Если не указан, транзакция отправится в текущую сеть кошелька.
messages — массив сообщений. TON позволяет отправлять до 4 сообщений в одной транзакции (ограничение внешнего сообщения кошелька v4/v5).
Работа с nanoTON
Суммы в TON Connect передаются в nanoTON (1 TON = 10^9 nanoTON) как строки, а не числа:
1 TON = '1000000000' (10^9)
0.1 TON = '100000000' (10^8)
0.05 TON = '50000000' (5 * 10^7)
0.001 TON = '1000000' (10^6)
Почему строки?
JavaScript Number может хранить целые числа точно только до 2^53 (примерно 9 * 10^15). Для больших сумм (миллионы TON) точность теряется. Строки и BigInt решают эту проблему:
// Правильно: BigInt
const amount = BigInt(0.1 * 1e9).toString(); // '100000000'
// Или используя @ton/core:
import { toNano } from '@ton/core';
const amount = toNano('0.1').toString(); // '100000000'
Используйте toNano() из @ton/core
Функция toNano('0.1') безопасно конвертирует строковое представление TON в nanoTON, избегая проблем с плавающей запятой. Не пишите 0.1 * 1e9 — результат может быть неточным.
Простой перевод TON
Перевод TON на другой адрес — минимальный запрос:
const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 300, // 5 минут
messages: [
{
address: 'EQBYivdc0GAk-nnczaMnYNuSjpeXu2nJS3DZ4KqLjosX5sVC',
amount: toNano('0.5').toString(),
},
],
};
const result = await tonConnectUI.sendTransaction(transaction);
Кошелёк получит запрос, покажет пользователю адрес получателя и сумму, и попросит подтверждение.
Payload: вызов смарт-контрактов
Для вызова метода смарт-контракта добавьте payload — BOC-encoded тело сообщения:
import { beginCell, toNano } from '@ton/core';
// Пример: вызов контракта с op-кодом и параметрами
const body = beginCell()
.storeUint(0x7362d09c, 32) // op: transfer (Jetton)
.storeUint(0, 64) // query_id
.storeCoins(toNano('100')) // amount of jettons
.storeAddress(recipientAddress) // destination
.storeAddress(senderAddress) // response_destination
.storeBit(false) // no custom payload
.storeCoins(toNano('0.01')) // forward_ton_amount
.storeBit(false) // no forward payload
.endCell();
const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 300,
messages: [
{
address: jettonWalletAddress, // адрес Jetton Wallet отправителя
amount: toNano('0.05').toString(), // газ для обработки
payload: body.toBoc().toString('base64'), // BOC в base64
},
],
};
const result = await tonConnectUI.sendTransaction(transaction);
Кошелёк может распознать известные op-коды (например, Jetton transfer) и показать пользователю понятное описание вместо raw-данных.
stateInit: деплой контракта
Если нужно развернуть новый контракт одновременно с отправкой сообщения, используйте поле stateInit:
const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 300,
messages: [
{
address: newContractAddress,
amount: toNano('0.1').toString(),
stateInit: stateInitBoc.toString('base64'), // код + начальные данные
payload: initMessageBody.toBoc().toString('base64'),
},
],
};
stateInit содержит код контракта и начальные данные. Адрес контракта вычисляется детерминистически из stateInit (как мы разбирали в уроке о Jetton Wallet).
Multi-message транзакции
TON позволяет отправлять до 4 сообщений в одной транзакции. Все сообщения отправляются атомарно — либо все, либо ни одного:
const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 300,
messages: [
{
address: recipientA,
amount: toNano('1').toString(),
},
{
address: recipientB,
amount: toNano('2').toString(),
},
{
address: contractC,
amount: toNano('0.05').toString(),
payload: somePayload.toBoc().toString('base64'),
},
],
};
Multi-message — это особенность TON Wallet v4/v5
Кошельки v4 и v5 поддерживают до 4 исходящих сообщений в одном внешнем сообщении. Более старые версии кошельков (v3) поддерживают только 1 сообщение. SDK автоматически проверяет поддержку.
Обработка ошибок
sendTransaction может завершиться ошибкой по нескольким причинам:
import { useTonConnectUI } from '@tonconnect/ui-react';
function SendButton() {
const [tonConnectUI] = useTonConnectUI();
const handleSend = async () => {
try {
const result = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 300,
messages: [
{
address: 'EQBYivdc0GAk-nnczaMnYNuSjpeXu2nJS3DZ4KqLjosX5sVC',
amount: toNano('0.1').toString(),
},
],
});
// Успех: result.boc содержит подписанную транзакцию
console.log('Транзакция отправлена:', result.boc);
} catch (error) {
// Пользователь отклонил транзакцию
// Или кошелёк отключился
// Или истёк validUntil
console.error('Ошибка:', error);
}
};
return <button onClick={handleSend}>Отправить</button>;
}
Типичные ошибки
| Ошибка | Причина | Решение |
|---|---|---|
| User rejected | Пользователь нажал “Отклонить” в кошельке | Информировать пользователя, не повторять автоматически |
| Timeout | Пользователь не ответил до истечения validUntil | Создать новую транзакцию с обновлённым validUntil |
| Connection lost | Bridge-соединение разорвано | Переподключить кошелёк через TonConnectButton |
| Bad request | Неверный формат сообщения | Проверить address, amount, payload |
Итоги
| Концепция | Описание |
|---|---|
sendTransaction | Метод SDK для отправки транзакции через кошелёк |
validUntil | Unix timestamp — защита от устаревших транзакций |
| nanoTON | Единица измерения: 1 TON = 10^9 nanoTON, передаётся как строка |
payload | BOC-encoded тело сообщения для вызова контрактов |
stateInit | BOC-encoded код + данные для деплоя нового контракта |
| Multi-message | До 4 сообщений в одной транзакции (v4/v5 кошельки) |
В следующем уроке разберём аутентификацию через ton_proof — механизм, который позволяет dApp убедиться, что пользователь действительно владеет указанным кошельком, без создания блокчейн-транзакции.
Частые ошибки
- Не показывают пользователю детали транзакции перед отправкой: адрес получателя, сумму и payload нужно отображать для верификации.
- Забывают обрабатывать timeout транзакции: если пользователь не подтвердил в кошельке за определённое время, нужно показать соответствующее состояние.
- Отправляют несколько транзакций параллельно без учёта seqno: каждая следующая транзакция должна иметь корректный sequence number.
- Не проверяют статус транзакции после отправки: нужно отслеживать подтверждение на блокчейне, а не считать транзакцию успешной после отправки в кошелёк.
Проверка знанийЧем payload отличается от обычного перевода TON, и зачем в amount указывать сумму при вызове контракта?
Check Your Understanding
Finished the lesson?
Mark it as complete to track your progress
Войдите чтобы оценить урок