Обработка сообщений
Резюме: В предыдущих шагах мы изменили взаимодействие нашего смарт-контракт с
хранилищем,get-методы, и изучили базовый поток разработки смарт-контрактов.
Теперь мы готовы перейти к основным функциям смарт-контрактов — отправке и получению сообщений. В TON сообщения используются не только для отправки валюты, но и в качестве механизма обмена данными между смарт-контрактами, что делает их ключевыми сущностями в разработке смарт-контрактов.
Если вы застряли на некоторых примерах, можете найти исходный шаблон проекта со всеми изменениями, внесёнными в этом руководстве, здесь.
Внутренние сообщения
Прежде чем приступить к реализации, давайте кратко опишем основные подходы и шаблоны, которые мы можем использовать для обработки внутренних сообщений.
Акторы и роли
Поскольку TON реализует модель акторов, довольно естественно думать об отношениях смарт-контрактов в контексте ролей, определяющих, кому доступна определённая функциональность смарт-контракта. Самые распространённые примеры ролей:
кто угодно(anyone): любой контракт без специально выделенной роли.владелец(owner): контракт, который обладает исключительным доступом к некоторым ключевым частям функциональности.
Давайте рассмотрим сигнатуру функции recv_internal, чтобы понять, как мы можем использовать это:
- FunC
- Tolk
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure
my_balance— баланс смарт-контракта при начале транзакции.msg_value— средства, полученные с сообщением.in_msg_full—ячейка, включающая «заголовочные» поля сообщения.in_msg_body— slice с телом сообщения.
fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice)
myBalance— баланс смарт-контракта при начале транзакции.msgValue— средства, полученные с сообщением.msgFull—cellс «заголовочными» полями сообщения.msgBody— slice с телом сообщения.
Вы можете найти полное описание отправки сообщений в этом разделе.
Из всего этого нам сейчас интересен адрес отправителя сообщения, который мы можем извлечь из ячейки msg_full. Получив этот адрес и сравнив его с хранимым, мы можем по соблюдению условия разрешить доступ к важнейшим частям функциональности нашего смарт-контракта. Обычный подход выглядит так:
- FunC
- Tolk
;; This is NOT a part of the project, just an example.
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
;; Parse the sender address from in_msg_full
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
;; check if message was send by owner
if (equal_slices_bits(sender_address, owner_address)) {
;;owner operations
return
} else if (equal_slices_bits(sender_address, other_role_address)){
;;other role operations
return
} else {
;;anyone else operations
return
}
;;no known operation were obtained for presented role
;;0xffff is not standard exit code, but is standard practice among TON developers
throw(0xffff);
}
// This is NOT a part of the project, just an example.
fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) {
// Parse the sender address from in_msg_full
var cs: slice = msgFull.beginParse();
val flags = cs.loadMessageFlags();
var sender_address = cs~load_msg_address();
if (isSliceBitsEqual(sender_address, owner_address)) {
// owner operations
return
} else if (isSliceBitsEqual(sender_address, other_role_address)){
// other role operations
return
} else {
// anyone else operations
return
}
throw 0xffff; // if the message contains an op that is not known to this contract, we throw
}
Операции
Популярный паттерн в TON — включать в тела сообщений 32-битный код операции, который сооб щает вашему контракту, какие действия необходимо выполнить:
- FunC
- Tolk
;; This is NOT a part of the project, just an example!
const int op::increment = 1;
const int op::decrement = 2;
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
;; Step 1: Check if the message is empty
if (in_msg_body.slice_empty?()) {
return; ;; Nothing to do with empty messages
}
;; Step 2: Extract the operation code
int op = in_msg_body~load_uint(32);
;; Step 3-7: Handle the requested operation
if (op == op::increment) {
increment(); ;;call to specific operation handler
return;
} else if (op == op::decrement) {
decrement();
;; Just accept the money
return;
}
;; Unknown operation
throw(0xffff);
}
//This is NOT a part of the project, just an example!
const op::increment : int = 1;
const op::decrement : int = 2;
fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) {
// Step 1: Check if the message is empty
if (slice.isEndOfSlice()) {
return; // Nothing to do with empty messages
}
// Step 2: Extract the operation code
var op = in_msg_body~load_uint(32);
// Step 3-7: Handle the requested operation
if (op == op::increment) {
increment(); //call to specific operation handler
return;
} else if (op == op::decrement) {
decrement();
// Just accept the money
return;
}
// Unknown operation
throw(0xffff);
}
Объединяя оба этих паттерна, вы можете достичь полного описания систем ваших смарт-контрактов, обеспечив безопасное взаимодействие между ними и раскрыв полностью потенциал модели акторов в TON.
Внешние сообщения
Внешние сообщения — это единственный способ запуска логики смарт-контракта извне блокчейна. Обычно нет необходимости реализовывать его в смарт-контрактах, потому что в большинстве случаев не требуется, чтобы внешние точки входа были доступны кому-либо, кроме вас. Если от внешних сообщений вам достаточно этой функциональности, то стандартным подходом будет делегировать эту ответственность отдельному актору — кошельку, кошельки в основном для этого и были спроектированы.
В разработке внешних эндпоинтов есть несколько стандартных подходов и мер безопасности, которые сейчас для вас могут быть излишне сложными. Так что в этом руководстве мы реализуем увеличение ранее добавленного числа ctx_counter_ext.
Эта реализация небезопасна и может привести к потере средств на вашем контракте. Не разворачивайте такой контракт в мейннете, особенно с крупной суммой средств на балансе.
Реализация
Давайте изменим наш смарт-контракт по стандартным шагам из раздела Обзор Blueprint, чтобы он мог получать внешние сообщения.
Шаг 1: редактирование кода смарт-контракта
Добавьте функцию recv_external в смарт-контракт HelloWorld:
- FunC
- Tolk
() recv_external(slice in_msg) impure {
accept_message();
var (ctx_id, ctx_counter, ctx_counter_ext) = load_data();
var query_id = in_msg~load_uint(64);
var addr = in_msg~load_msg_addr();
var coins = in_msg~load_coins();
var increase_by = in_msg~load_uint(32);
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(coins)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op::increase, 32)
.store_uint(query_id, 64)
.store_uint(increase_by, 32);
send_raw_message(msg.end_cell(), 0);
ctx_counter_ext += increase_by;
save_data(ctx_id, ctx_counter, ctx_counter_ext);
return ();
}
fun acceptExternalMessage(): void
asm "ACCEPT";
fun onExternalMessage(inMsg: slice) {
acceptExternalMessage();
var (ctxId, ctxCounter, ctxCounterExt) = loadData();
var queryId = inMsg.loadUint(64);
var addr = inMsg.loadAddress();
var coins = inMsg.loadCoins();
var increaseBy = inMsg.loadUint(32);
var msg = beginCell()
.storeUint(0x18, 6)
.storeSlice(addr)
.storeCoins(coins)
.storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.storeUint(OP_INCREASE, 32)
.storeUint(queryId, 64)
.storeUint(increaseBy, 32);
sendRawMessage(msg.endCell(), 0);
ctxCounterExt += increaseBy;
saveData(ctxId, ctxCounter, ctxCounterExt);
return ();
}
Эта функция при получении внешнего сообщения увеличит нашу переменную ctx_counter_ext и отправит внутреннее сообщение на указанный адрес с операцией increase.
Проверьте, что код смарт-контракта верен, запустив:
npx blueprint build
Ожидаемый вывод должен выглядеть примерно так:
✅ Compiled successfully! Cell BOC result:
{
"hash": "310e49288a12dbc3c0ff56113a3535184f76c9e931662ded159e4a25be1fa28b",
"hashBase64": "MQ5JKIoS28PA/1YROjU1GE92yekxZi3tFZ5KJb4foos=",
"hex": "b5ee9c7241010e0100d0000114ff00f4a413f4bcf2c80b01020120020d02014803080202ce0407020120050600651b088831c02456f8007434c0cc1caa42644c383c0040f4c7f4cfcc4060841fa1d93beea5f4c7cc28163c00b817c12103fcbc2000153b513434c7f4c7f4fff4600017402c8cb1fcb1fcbffc9ed548020120090a000dbe7657800b60940201580b0c000bb5473e002b70000db63ffe002606300072f2f800f00103d33ffa40fa00d31f30c8801801cb055003cf1601fa027001cb6a82107e8764ef01cb1f12cb3f5210cb1fc970fb0013a012f0020844ca0a"
}
✅ Wrote compilation artifact to build/HelloWorld.compiled.json