Исполнение на основе сообщений
В этом разделе рассмотрено, как в TON сообщения инициируют транзакции. Вы узнаете, как аккаунты общаются с помощью сообщений, чем различаются типы сообщений, а также как конструировать и отправлять сообщения, чтобы запустить исполнение кода в блокчейне.
Сообщения
В блокчейне TON сообщение — это основная единица взаимодействия аккаунтов (смарт-контрактов). Все действия, изменения состояния и выполнение логики в аккаунтах инициируются сообщениями.
Транзакции и сообщения на жизненном примере
Давайте разберём, как работают транзакции и сообщения в TON, при помощи аналогии. Представьте себе блокчейн TON как уникальный город в глобальном мире — интернете. В этом городе строгие правила: жители никогда не встречаются лично, и единственный способ общения — почта. Люди получают сообщения с просьбой выполнить задание, выполняют его у себя дома, вдали от чужих глаз, а затем отправляют результат другим.
Каждый человек при проверке своего почтового ящика забирает ровно одно входящее сообщение, затем запирается дома и не выходит, пока задание не будет выполнено. Пока они работают, могут приходить новые письма, но во время работы они полностью их игнорируют.
Если посреди выполнения задания они понимают, что не хватает какой-то необходимой информации, они не могут сделать паузу для уточнений. В этом случае они должны объявить попытку выполнения провалившейся, и, в лучшем случае, вернуть остатки средств с первоначальными инструкциями.
Если задание выполнено успешно, то человек в соответствии с указаниями исходного сообщения:
- отправляет новые сообщения другим жителям, или
- надёжно сохраняет полученный результат у себя дома.
После завершения задачи человек возвращается к своему почтовому ящику и забирает следующее входящее сообщение.
Теперь представьте, что в ходе каждой рабочей сессии человек ведёт подробный журнал. Чтобы рабочие задачи в нём были организованы и разделены, каждая запись в журнале называется транзакцией. Каждая транзакция содержит:
- полное содержание входящего сообщения (задание),
- заметку о том, что было создано и где это хранится,
- и, опционально, информацию о любых сообщениях, отправленных другим.
Сообщения, доставляемые почтой между жителями, называются внутренними сообщениями (internal messages). В город также поступают сообщения из внешнего мира. У них указан получатель, но отправитель неизвестен. Их называют внешними входящими (external incoming) или сообщениями извне (external-in messages).
Хотя с точки зрения актора внешние и внутренние сообщения выполняют схожую роль, он может обрабатывать их по-разному. Некоторые акторы могут обрабатывать всё, в то время как другие могут полностью игнорировать сообщения из внешнего мира — словно человек, отказывающийся открывать письма от незнакомцев.
Структура сообщения: TL-B
Теперь давайте посмотрим на данные, содержащиеся в сообщениях. В TON различаются внешние и внутренние сообщения, однако оба этих типа способны переносить данные.
В TON все данные представлены с использованием ячеек. Для сериализации данных в ячейку используется устоявшийся стандарт под названием TL-B (Type Language – Binary).
Нативная структура сообщения
В TON все сообщения соответствуют единой схеме, которая определяет их структуру и сериализацию.
message$_ {X:Type} info:CommonMsgInfo
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = Message X;
info: CommonMsgInfo
— содержит метаданные о сообщении.init: (Maybe (Either StateInit ^StateInit))
— необязательное поле, используемое для инициализации нового аккаунта или о бновления существующего.body: (Either X ^X)
— основное содержание сообщения; может быть либо встроено напрямую в ячейку, либо представлено ссылкой на другую ячейку.
Типы сообщений
В TON существуют три типа сообщений:
- Внешнее входящее: отправлено извне блокчейна → получено смарт-контрактом
- Внутреннее: отправлено смарт-контрактом → получено смарт-контрактом
- Внешнее исходящее: отправлено смарт-контрактом → получено вне блокчейна (неизвестным актором)
Внешние входящие сообщения
Функциональная роль
Внешние входящие (external incoming) сообщения, они же сообщения извне (external-in), служат внешнему миру основной точкой входа для взаимодействия с блокчейном TON. Любой пользователь может отправить произвольные данные любому смарт-контракту, и дальше от контракта зависит, как они будут обработаны. Технически любой аккаунт может получать внешние сообщения. Однако то, как конкретный контракт обрабатывает их, полностью зависит от его внутренней логики. Самый распространённый тип контрактов, поддерживающих работу с такими сообщениями — контракт кошелька.
Среди типичных источников внешних входящих сообщений есть такие:
- пользователи кошельков
- валидаторы
- dApp-сервисы
Хотя внешние входящие сообщения редко используются в основной логике смарт-контрактов, они необходимы для интеграции внешних действий в блокчейн. Любое взаимодействие, которое исходит извне TON — например, отправка TON или использование DEX — начинается с внешнего входящего сообщения. Самый распространённый пример этого — контракт кошелька, который получает инструкцию от пользователя, а затем передаёт её, отправляя внутренние сообщения другим контрактам.
Отправка внешнего входящего сообщения
Термин сообщение тесно связан с транзакцией, но у них разные цели и их не следует путать:
- Сообщения — это пакеты данных с инструкциями для действий, которыми обмениваются смарт-контракты.
- Транзакция — это результат исполнения смарт-контракта в ответ на входящее сообщение. Во время выполнения контракт может обновить своё состояние и создать одно или несколько исходящих сообщений.
Внешнее входящее сообщение — это такое сообщение, чей заголовок CommonMsgInfo
использует структуру ext_in_msg_info$10
.
message$_ {X:Type} info:CommonMsgInfo
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = Message X;
TL-B:
//external incoming message
ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt
import_fee:Grams = CommonMsgInfo;
Поскольку внешнее входящее сообщение приходит в блокчейн извне, инициатором является внешний актор. Это может быть либо контракт кошелька, либо специализированный код, взаимодействующий с API-сервисами блокчейна.
Чтобы отправить внешнее входящее сообщение смарт-контракту, вам необходимо:
- Сконструировать структуру данных, соответствующую схеме TL-B
- Отправить эту структуру в блокчейн с помощью сервиса API
Например, в Blueprint есть встроенный помощник, который динамически со бирает эту структуру для разработчика.
//@ton/blueprint 0.36.1
import { Address, beginCell } from '@ton/ton';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider, args: string[]) {
//Mainnet address : 'EQAyVZ2rDnEDliuaQJ3PJFKiqAS-9fOm9s7DG1y5Ta16zwU2'
const address = Address.parse('EQAyVZ2rDnEDliuaQJ3PJFKiqAS-9fOm9s7DG1y5Ta16zwU2');
//Switch address for the Testnet :
//const address = Address.parse('kQAyVZ2rDnEDliuaQJ3PJFKiqAS-9fOm9s7DG1y5Ta16z768');
// Get current seqno using blueprint's provider
const contractProvider = provider.provider(address);
const result = await contractProvider.get('seqno', []);
const currentSeqno = result.stack.readNumber();
// Send external message with current seqno
return contractProvider.external(beginCell().storeUint(currentSeqno, 32).endCell());
}
- Подготовьте приложение-кошелёк (например, Tonkeeper), которое вы будете использовать д ля отправки внешнего сообщения
- Создайте локально проект с помощью Blueprint:
npm create ton@latest
- Добавьте предоставленный скрипт в ваш проект Blueprint в каталог
scripts
, например, так:blueprintproject/scripts/yourscript.ts
- Запустите скрипт следующей командой:
npx blueprint run yourscript
Внутренние сообщения
Функциональная роль
Внутреннее сообщение в блокчейне TON — это сообщение, отправленное одним смарт-контрактом другому. Когда контракт получает внутреннее сообщение, он может надёжно определить:
- сколько монет TON содержится в сообщении
- какие контракты являются отправителем и получателем
Блокчейн обеспечивает целостность этой информации, позволяя доверять контракту и безопасно его использовать. TL-B:
Внутренние сообщения всегда инициированы контрактом — они являются результатом выполнения транзакции. Другими словами, как следует из названия, отправителем внутреннего сообщения всегда является смарт-контракт.
Самый распространённый способ отправки внутреннего сообщения — сначала отправить внешнее сообщение контракту, который содержит логику для пересылки внутреннего сообщения другому контракту.
//internal message
int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
src:MsgAddressInt dest:MsgAddressInt
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
created_lt:uint64 created_at:uint32 = CommonMsgInfo;
Структура | Описание |
---|---|
int_msg_info$0 | Указывает, что сообщение внутреннее. Тег $0 означает, что CommonMsgInfo начинается с бита 0. |
ihr_disabled | Флаг, указывающий, отключена ли маршрутизация Instant Hypercube Routing (IHR). |
bounce | Если установлено значение 1, при ошибке обработки сообщение будет возвращено. |
bounced | Флаг, указывающий, является ли само сообщение результатом такого возврата. |
src | Адрес смарт-контракта, отправившего сообщение. |
dest | Адрес смарт-контракта, которому предназначено сообщение. |
value | Структура, описывающая сумму и тип средств, переданных в сообщении. |
ihr_fee | Комиссия за доставку через IHR. |
fwd_fee | Комиссия за пересылку сообщения. |
created_lt | Логическое время создания сообщения, используется для упорядочивания действий контракта. |
created_at | Время создания сообщения в формате временной метки Unix. |
В первую очередь разработчику требуется указать value
(сумму Toncoin, прикреплённую к внутреннему сообщению) и dest
(адрес назначения).
В этом примере контракту кошелька отправляют внешнее сообщени е с дополнительными инструкциями по отправке внутреннего сообщения. В результате обработки транзакции контракт кошелька отправляет исходящее внутреннее сообщение с указанными значениями value
и адресом получателя address
.
//@ton/blueprint 0.36.1
import { Address } from '@ton/ton';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider, args: string[]) {
const address = Address.parse('kQBUCuxgGsF6znHM_yNmnV_EwtlmdvmDzqTxiWHJip2ux6Wn');
const contractProvider = provider.provider(address);
return contractProvider.internal(provider.sender(), {
value: '0.01',
});
}
Внешние исходящие сообщения
Внешние исходящие сообщения (также известные как логи) — особый тип сообщений, генерируемых смарт-контрактами. Они не адресованы какому-либо конкретному получателю. Вместо этого они служат логами или сигналами, которые можно отслеживать вне блокчейна. Хотя эти сообщения редко используются в основной логике смарт-контрактов, они важны для таких задач, как:
- индексация и мониторинг состояния блокчейна
- запуск процессов вне блокчейна, вроде уведомления внешних служб или обновления пользовательского интерфейса
Например, контракт децентрализованной биржи (DEX) может выпустить внешнее исходящее сообщение после успешного обмена токенов, чтобы уведомить внешние системы или пользователей о событии.
TL-B:
//external outgoing message
ext_out_msg_info$11 src:MsgAddressInt dest:MsgAddressExt
created_lt:uint64 created_at:uint32 = CommonMsgInfo;
Возврат сообщений
Возвратное сообщение (bounce message) автоматически отправляется обратно отправителю, если транзакция завершается неудачей: например, смарт-контракт получателя не существует или его выполнение завершается ошибкой. Этот механизм не только сигнализирует о сбое, но и позволяет отправителю отреагировать соответствующим образом, например, вернув токены или отобразив пользователю сообщение об ошибке.
Большинство внутренних сообщений между смарт-контрактами должны быть возвращаемыми (то есть у них должен быть установлен флаг bounce), чтобы в случае сбоя:
- сообщение возвращалось отправителю
- любые оставшиеся средства после вычета комиссий возвращались
Смарт-ко нтракты, в свою очередь, должны:
- проверять флаг bounced во входящих сообщениях
- и либо игнорировать возвращённые сообщения (завершать исполнение с
code = 0
) - либо явно обрабатывать ошибку
Вы можете запустить в Blueprint приведённый далее скрипт, чтобы визуально сравнить различия между возвращаемыми и невозвращаемыми сообщениями.
Используйте один из следующих способов для запуска скрипта:
Вариант 1: с помощью переменных окружения в командной строке
WALLET_MNEMONIC="unfold your mnemonics ... is added here" \
WALLET_VERSION="v4r2" \
npx blueprint run sendBounceableMessages --testnet --mnemonic
Вариант 2: с помощью файла .env
Создайте файл .env
в исходном каталоге:
WALLET_MNEMONIC="unfold your mnemonics ... is added here"
WALLET_VERSION="v4r2"
Затем запустите:
npx blueprint run sendBounceableMessages --testnet --mnemonic
Примечание: В данном случае невозможно использовать TON Connect (например, с помощью флага --tonconnect
), поскольку кошельки вроде Tonkeeper ради безопасности не позволяют dApp-приложениям контролировать флаг bounce
. Они всегда отправляют возвращаемые сообщения. Чтобы сохранить этот флаг, вам необходимо создать и подписать сообщение вручную, используя мнемоническую фразу.
Отправить возвращаемое сообщение
import { Address, toNano, beginCell, internal } from '@ton/core';
import { NetworkProvider } from '@ton/blueprint';
import { mnemonicToWalletKey } from '@ton/crypto';
import { WalletContractV3R2, WalletContractV4 } from '@ton/ton';
export async function run(provider: NetworkProvider): Promise<void> {
// --- Get wallet data from environment variables ---
const mnemonic = process.env.WALLET_MNEMONIC;
const versionStr = process.env.WALLET_VERSION || 'v4r2';
if (!mnemonic) {
throw new Error('WALLET_MNEMONIC environment variable is not set. Please see instructions in the script file.');
}
// --- Initialize wallet from mnemonic ---
const keyPair = await mnemonicToWalletKey(mnemonic.split(' '));
const secretKey = keyPair.secretKey;
let wallet;
if (versionStr === 'v3r1' || versionStr === 'v3r2') {
wallet = WalletContractV3R2.create({ workchain: 0, publicKey: keyPair.publicKey });
} else if (versionStr === 'v4r1' || versionStr === 'v4r2') {
wallet = WalletContractV4.create({ workchain: 0, publicKey: keyPair.publicKey });
} else {
throw new Error(
'Unsupported wallet version: ' + versionStr + '. Supported versions are v3r1, v3r2, v4r1, v4r2.',
);
}
const myAddress = wallet.address;
console.log(`Using wallet ${versionStr} at address: ${myAddress.toString()}`);
// --- Check if wallet is deployed ---
if (!(await provider.isContractDeployed(myAddress))) {
console.log('Wallet is not deployed. Please send some TON to this address and try again.');
return;
}
// This is a valid address, but its purpose is for burning.
// 0:0000000000000000000000000000000000000000000000000000000000000000
// https://ton.org/address/
// We will send messages to it to demonstrate bounce handling.
const burnAddress = Address.parse('EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c');
// --- Get current seqno ---
const walletProvider = provider.provider(myAddress);
let seqno = await wallet.getSeqno(walletProvider);
console.log(` Current seqno: ${seqno}`);
// --- Create messages ---
// This message is sent to a burn address, but it is marked as "bounceable".
// The transaction will fail on the recipient's side (as it's just a blackhole),
// and the value (minus fees) will be returned to the sender.
const bounceableMessage = internal({
to: burnAddress,
value: toNano('0.005'),
body: beginCell().storeUint(0, 32).storeStringTail('bounceable test').endCell(),
bounce: true,
});
// This message is also sent to a burn address, but it is marked as "non-bounceable".
// The value sent with this message will be lost forever.
const nonBounceableMessage = internal({
to: burnAddress,
value: toNano('0.005'),
body: beginCell().storeUint(0, 32).storeStringTail('non-bounceable test').endCell(),
bounce: false,
});
// --- Create a transfer with both messages ---
console.log('Sending one external message with two internal messages (one bounceable, one non-bounceable)...');
const transfer = wallet.createTransfer({
seqno: seqno,
secretKey: secretKey,
messages: [bounceableMessage, nonBounceableMessage], // Send both in one transaction
});
await walletProvider.external(transfer);
console.log('External message sent successfully!');
}
Ожидаемый вывод в консоли
Using file: sendBounceableMessages
Connected to wallet at address: 0QD3oTH51Tp4UNhCLfX3axyG2X9H_9wZpLoC7W--WbXwxYkp
Using wallet v4r2 at address: EQD3oTH51Tp4UNhCLfX3axyG2X9H_9wZpLoC7W--WbXwxW9m
Current seqno: 36
Sending one external message with two internal messages (one bounceable, one non-bounceable)...
External message sent successfully!
Содержимое сообщения при возврате не должно выполняться как обычный запрос; оно предназначено только для сигнализации об ошибке и возврата средств.
Произвольная структура сообщения
Мы рассмотрели нативные механизмы коммуникации в TON: внешние и внутренние сообщения.
Чтобы инициировать в смарт-контракте определённую логику, необходимо прикрепить к стандартному сообщению полезную нагрузку (payload).
При создании внутренних сообщений эту необязательную полезную нагрузку включают в поле body
.
Существует два основных способа определить структуру тела сообщения:
- На основе схемы TL-B, предоставленной в документации или исходном коде контракта.
- С помощью анализа кода контракта и реверсирования логики десериализации.
Сериализация внутренних сообщений в соответствии с TL-B
Возьмём в качестве примера контракт nft-item.fc
. Стандарт определяет схему TL-B для сообщения, необходимого для передачи права собственности на NFT другому пользователю.
С технической точки зрения, это внутреннее сообщение, отправленное с контракта текущего владельца, содержащее код операции для передачи и адрес нового владельца.
transfer#5fcc3d14 query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress
custom_payload:(Maybe ^Cell) forward_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) = InternalMsgBody;`
TON SDK предоставляет предопределённые примитивы для самых распространённых структур, поэтому сложные структуры обычно кодируются с помощью встроенных помощников.
import { Address, beginCell, toNano } from '@ton/ton';
/*
`transfer#5fcc3d14 query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress
custom_payload:(Maybe ^Cell) forward_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) = InternalMsgBody;`
*/
const messageBody = beginCell()
.storeUint(0x5fcc3d14, 32) // opcode for NFT transfer
.storeUint(0, 64) // query id, by default 0
.storeAddress(destinationAddress) //
.storeAddress(destinationAddress) // response destination for lasts funds
.storeBit(0) // if 0, no custom payload
.storeCoins(toNano('0.01')) // forward amount - if >0, will send additional message to destination as a wallet notification message
.endCell();
Для тестирования процесса отправки доступен предварительно настроенный пример. Чтобы запустить его, вам необходимо указать адреса кошелька и контракта NFT.
//@ton/blueprint 0.36.1
import { Address, beginCell, toNano } from '@ton/ton';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider, args: string[]) {
// PREPARE INTERNAL MESSAGE TRANSFER
const nftItemAddress = Address.parse('kQBUCuxgGsF6znHM_yNmnV_EwtlmdvmDzqTxiWHJip2ux6Wn');
const destinationAddress = Address.parse('0QABa48hjKzg09hN_HjxOic7r8T1PleIy1dRd8NvZ3922CW7');
const address = nftItemAddress;
const contractProvider = provider.provider(address);
/*
`transfer#5fcc3d14 query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress
custom_payload:(Maybe ^Cell) forward_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) = InternalMsgBody;`
*/
const messageBody = beginCell()
.storeUint(0x5fcc3d14, 32) // opcode for NFT transfer
.storeUint(0, 64) // query id
.storeAddress(destinationAddress)
.storeAddress(destinationAddress) // response destination
.storeBit(0) // if 0, no custom payload
.storeCoins(toNano('0.01')) // forward amount - if >0, will send additional message to destination as a wallet notification message
.endCell();
return contractProvider.internal(provider.sender(), {
value: toNano('0.5'),
body: messageBody,
});
}
Сериализация внутреннего сообщения в соответствии с кодом контракта
Предположим, вам нужно составить сообщение для смарт-контракта. В качестве примера мы используем контракт Jetton wallet и контракт кошелька версии V3. Есть два основных типа функций, принимающих сообщения:
Чтобы правильно сериализовать внутреннее сообщение для этого смарт-контракта, мы должны изучить, как он десериализует входящие сообщения.
Сначала мы изучим, как контракт обрабатывает операцию передачи, разбирая непосредственно тело внутреннего сообщения:
() send_jettons(slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure inline_ref {
;; see transfer TL-B layout in jetton.tlb
int query_id = in_msg_body~load_query_id();
int jetton_amount = in_msg_body~load_coins();
slice to_owner_address = in_msg_body~load_msg_addr();
check_same_workchain(to_owner_address);
(int status, int balance, slice owner_address, slice jetton_master_address) = load_data();
throw_unless(error::not_owner, equal_slices_bits(owner_address, sender_address));
balance -= jetton_amount;
throw_unless(error::balance_error, balance >= 0);
cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, my_code());
slice to_wallet_address = calculate_jetton_wallet_address(state_init);
slice response_address = in_msg_body~load_msg_addr();
in_msg_body~skip_maybe_ref(); ;; custom_payload
int forward_ton_amount = in_msg_body~load_coins();
check_either_forward_payload(in_msg_body);
slice either_forward_payload = in_msg_body;
}
Теперь давайте посмотрим, как эта функция вызывается во время обработки входящих сообщений:
() recv_internal(int my_ton_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice in_msg_full_slice = in_msg_full.begin_parse();
int msg_flags = in_msg_full_slice~load_msg_flags();
if (msg_flags & 1) { ;; is bounced
on_bounce(in_msg_body);
return ();
}
slice sender_address = in_msg_full_slice~load_msg_addr();
int fwd_fee_from_in_msg = in_msg_full_slice~retrieve_fwd_fee();
int fwd_fee = get_original_fwd_fee(MY_WORKCHAIN, fwd_fee_from_in_msg); ;; we use message fwd_fee for estimation of forward_payload costs
int op = in_msg_body~load_op();
;; outgoing transfer
if (op == op::transfer) {
send_jettons(in_msg_body, sender_address, msg_value, fwd_fee);
return ();
}
}
Теперь нам нужно создать валидное сообщение, оттолкнувшись от содержимого этой функции:
Чтобы запустить скрипт и отправить внутреннее сообщение, выполните следующие шаги:
- Запросите тестнет-монеты TON у бота https://t.me/testgiver_ton_bot
- Запросите тестнет-версию USDT у бота https://t.me/testnet_usdt_giver_bot
- Замените
jettonWalletAddress
в скрипте на ваш собственный адрес контракта Jetton wallet. - Запустите скрипт
npx blueprint run sendInternalMessage --testnet --tonconnect
Отправить внутреннее сообщение
import { Address, toNano, beginCell } from '@ton/core';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider): Promise<void> {
const sender = provider.sender();
const myAddress = sender.address;
if (!myAddress) {
throw new Error('Wallet not connected!');
}
console.log(`Sending internal message from wallet: ${myAddress.toString()}`);
// Attention: Replace with your real Jetton Wallet address
const jettonWalletAddress = Address.parse('0QD3oTH51Tp4UNhCLfX3axyG2X9H_9wZpLoC7W--WbXwxYkp');
const jettonTransferBody = beginCell()
.storeUint(0xf8a7ea5, 32) // op::internal_transfer
.storeUint(0, 64) // query_id
.storeCoins(1) // jetton_amount 0.000001 for testnet USDT
.storeAddress(myAddress) // to_address (sending to myself)
.storeAddress(myAddress) // response_address for excesses
.storeMaybeRef(null) // custom_payload
.storeCoins(toNano('0.000000001')) // forward_ton_amount lowest possible amount to get jetton notify message
.storeMaybeRef(null) // forward_payload
.endCell();
// This wrapper handles basic internal message
await sender.send({
to: jettonWalletAddress,
value: toNano('0.02'), // To handle gas
body: jettonTransferBody,
});
console.log('Internal message on Jetton Wallet successfully sent!');
}
// TEP - 74: https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md
/*
transfer#0f8a7ea5
query_id:uint64
amount:Coins
destination:MsgAddress
response_destination:MsgAddress
custom_payload:(Maybe ^Cell)
forward_ton_amount:Coins
forward_payload:(Either Cell ^Cell)
= JettonMsg;
*/
Хотя вы можете использовать blueprint --tonconnect
для отправки внутренних сообщений, этот подход не поддерживает внешние сообщения.
Это ограничение вызвано тем, что TON Connect не предоставляет низкоуровневые возможности подписи сообщений по следую щим причинам:
- Безопасность заложена в фундамент: кошельки (например, Tonkeeper) не предоставляют dApp приватные ключи в исходном виде.
- Абстракция TON Connect: провайдер скрывает внутреннюю реализацию кошелька за единым интерфейсом.
- Ограниченные возможности API: команда blueprint предназначена для развёртывания и тестирования контрактов, а не для ручной подписи сообщений.
Вот почему в случае с blueprint вместо TON Connect мы используем подход с мнемонической фразой:
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512);
var cs = in_msg;
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
throw_if(35, valid_until <= now());
var ds = get_data().begin_parse();
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
ds.end_parse();
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, subwallet_id == stored_subwallet);
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
accept_message();
cs~touch();
while (cs.slice_refs()) {
var mode = cs~load_uint(8);
send_raw_message(cs~load_ref(), mode);
}
}
Для запуска следующего скрипта используйте один из этих способов:
Вариант 1: использование переменных окружения в командной строке
WALLET_MNEMONIC="unfold your mnemonics ... is added here" \
WALLET_VERSION="v4r2" \
npx blueprint run sendExternalMessage --testnet --mnemonic
Вариант 2: использование файла .env
Создайте файл .env
в исходном каталоге:
WALLET_MNEMONIC="unfold your mnemonics ... is added here"
WALLET_VERSION="v4r2"
Затем запустите:
npx blueprint run sendExternalMessage --testnet --mnemonic
Отправить внешнее сообщение
import { NetworkProvider } from '@ton/blueprint';
import { beginCell, toNano } from '@ton/core';
import { sign, mnemonicToWalletKey } from '@ton/crypto';
import { WalletContractV3R2, WalletContractV4 } from '@ton/ton';
export async function run(provider: NetworkProvider): Promise<void> {
// Get wallet data from environment variables
const mnemonic = process.env.WALLET_MNEMONIC;
const versionStr = process.env.WALLET_VERSION || 'v4r2';
if (!mnemonic) {
throw new Error('WALLET_MNEMONIC environment variable is not set');
}
// Initialize wallet from mnemonic
const keyPair = await mnemonicToWalletKey(mnemonic.split(' '));
const secretKey = keyPair.secretKey;
let wallet;
if (versionStr === 'v3r1' || versionStr === 'v3r2') {
wallet = WalletContractV3R2.create({ workchain: 0, publicKey: keyPair.publicKey });
} else if (versionStr === 'v4r1' || versionStr === 'v4r2') {
wallet = WalletContractV4.create({ workchain: 0, publicKey: keyPair.publicKey });
} else {
throw new Error(
'Unsupported wallet version: ' + versionStr + '. Supported versions are v3r1, v3r2, v4r1, v4r2.',
);
}
const myAddress = wallet.address;
console.log(`Using wallet ${versionStr} at address: ${myAddress.toString()}`);
if (!(await provider.isContractDeployed(myAddress))) {
console.log('Wallet is not deployed. Please send some TON to this address and try again.');
return;
}
// Get current seqno
const walletProvider = provider.provider(myAddress);
const seqno = await wallet.getSeqno(walletProvider);
console.log(` Current seqno: ${seqno}`);
// Create an internal message (a simple transfer to yourself)
const internalMessage = beginCell()
.storeUint(0x10, 6) // flags: ihrDisabled, bounce, bounced
.storeAddress(myAddress)
.storeCoins(toNano('0.01'))
.storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) // empty simple message body
.endCell();
// Create an unsigned external message for signing
const subwalletId = 698983191; // Standard subwallet ID
const validUntil = Math.floor(Date.now() / 1000) + 60; // 60 seconds from now
// Var (subwallet_id, valid_until, msg_seqno) from func
const toSign = beginCell().storeUint(subwalletId, 32).storeUint(validUntil, 32).storeUint(seqno, 32);
// Add 'op' field for v4 wallets
if (versionStr.startsWith('v4')) {
toSign.storeUint(0, 8); // op = 0 for simple transfer
}
toSign
.storeUint(3, 8) // var mode from func
.storeRef(internalMessage); // cs~load_ref() from func
const toSignCell = toSign.endCell();
// Sign the message hash
const signature = sign(toSignCell.hash(), secretKey);
console.log(` Message signed`);
// Create final external message body with signature
const body = beginCell()
.storeBuffer(signature) // var signature
.storeBuilder(toSign) // in_msg
.endCell();
console.log(` Sending external message...`);
// Send the external message to the network
await provider.provider(myAddress).external(body);
console.log(' Message sent successfully!');
}
Сериализация внутренних сообщений в соответствии с дизассемблированным кодом
Если у разработчика нет доступа к схемам TL-B или исходному коду контракта, единственным доступным вариантом является анали з дизассемблированного кода контракта.
Этот метод является наименее эффективным и наиболее провоцирующем ошибки, но может быть полезен при работе с устаревшими или недокументированными контрактами.
Пример: Простой контракт
Следующий шаг
Мы только что рассмотрели, как сообщения функционируют в качестве основного механизма взаимодействия в TON — как их структурируют, передают и используют для инициирования транзакций.
Теперь давайте посмотрим, как сообщения и транзакции однозначно идентифицируются и отслеживаются — через их хеши.
Отслеживайте сообщения и транзакция с помощью хешей