Урок 3 Прокси смарт-контракт
Введение
В этом уроке мы напишем прокси (пересылает все сообщения его владельцу) смарт-контракт в блокчейне TON на языке FunC, а протестируем его уже в следующем уроке.
Требования
Для прохождения данного урока вам достаточно установить Node.js. Желательно устанавливать одну из последних версий, например 18.
А также уметь создавать/деплоить проект с помощью Blueprint. Научиться этому можно в первом уроке.
Смарт-контракт
Смарт-контракт, который мы будем делать, должен обладать следующей функциональностью**:
- Пересылка всех сообщений поступающих в контракт владельцу
- При пересылке сначала должен идти адрес отправителя, а потом тело оригинального сообщения
- Значение Toncoin, прикрепленное к пересылаемому сообщению, должно быть равно значению входящего сообщения за вычетом комиссий
- Адрес владельца хранится в хранилище смарт-контракта
- При отправке сообщения в контракт от владельца пересылка не должна осуществляться
** идеи для смарт-контрактов я решил брать из задач FunC contest1, так как они очень хорошо подходят для ознакомления с разработкой смарт-контрактов для TON.
Внешний метод
Для того, чтобы наш контракт могла принимать сообщения будем использовать функцию recv_internal()
, которая уже будет находиться в файле с FunC кодом после создания проекта.
() recv_internal(int balance, int msg_value, cell in_msg_full, slice in_msg_body) {
}
Адрес отправителя
В соотвествии с заданием нам необходимо взять адрес оправителя. Брать адресс мы будем из ячейки с входящим сообщением in_msg_full
. Код для этого действия вынесем в отдельную функцию.
() recv_internal (int balance, int msg_value, cell in_msg_full, slice in_msg_body) {
slice sender_address = parse_sender_address(in_msg_full);
}
Пишем функцию
Напишем код функции parse_sender_address
, которая из ячейки сообщения берет адрес отправителя и разберем его:
slice parse_sender_address (cell in_msg_full) inline {
var cs = in_msg_full.begin_parse();
var flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
return sender_address;
}
Как вы можете видеть функция имеет inline
спецификатор, ее код фактически подставляется в каждом месте вызова функции. Данный спецификатор полезно использовать в случаях, когда функция вызывается только в одном единственном месте.
Чтобы мы могли взять адрес, нам необходимо преобразовать ячейку в слайс c помощью begin_parse
:
var cs = in_msg_full.begin_parse();
Теперь нам надо пропустить первые 4 бита в этом слайсе, отведённые на флаги сообщения. С помощью load_uint
функции из стандартной бибилотеки FunC, которая загружает из слайса целое беззнаковое число размером N бит.
var flags = cs~load_uint(4);
В данном уроке мы не будем останавливаться подробно на флагах, но подробнее можно прочитать в документации.
Ну и наконец-то адрес. Используем load_msg_addr()
- которая загружает из слайса префикс, который является допустимым MsgAddress
(адресом).
slice sender_address = cs~load_msg_addr();
return sender_address;
Адрес получателя
Адрес будем брать из данных контракта. Про это мы уже говорили в предыдущих уроках.
Будем использовать:
get_data
- берет ячейку из данных контракта.
begin_parse
- ячейку преобразует в slice.
load_msg_addr()
- загружает из слайса префикс, который является допустимым MsgAddress
.
По итогу получаем следующую функцию:
slice load_data () inline {
var ds = get_data().begin_parse();
return ds~load_msg_addr();
}
Остается её только вызвать:
slice load_data () inline {
var ds = get_data().begin_parse();
return ds~load_msg_addr();
}
slice parse_sender_address (cell in_msg_full) inline {
var cs = in_msg_full.begin_parse();
var flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
return sender_address;
}
() recv_internal (int balance, int msg_value, cell in_msg_full, slice in_msg_body) {
slice sender_address = parse_sender_address(in_msg_full);
slice owner_address = load_data();
}
Проверим условие равенства адресов
По условию задачи, прокси не должна пересылать сообщение если оно исходит от владельца. Поэтому нам необходимо сравнить два адреса.
Функция Сравнения
Некоторые функции не объявлены в стандартной библиотеке, поэтому их приходится объявлять вручную, используя TVM-инструкции.
FunC поддерживает определение функции на ассемблере (имеется ввиду Fift). Происходит это следующим образом - мы определяем функцию как низкоуровневый примтив TVM. Для функции сравнения это будет выглядеть так:
int equal_slices (slice a, slice b) asm "SDEQ";
Как вы можете видеть, используется ключевое слово asm
.
Унарный оператор
Итак нашу функцию equal_slices
мы будем использовать в if
:
() recv_internal (int balance, int msg_value, cell in_msg_full, slice in_msg_body) {
slice sender_address = parse_sender_address(in_msg_full);
slice owner_address = load_data();
if equal_slices(sender_address, owner_address) {
}
}
Но функция проверят именно равенство, как проверить неравенство? Здесь может помочь унарный оператор ~
, который являетя побитовым “не”.
Теперь наш код выглядит так:
int equal_slices (slice a, slice b) asm "SDEQ";
slice load_data () inline {
var ds = get_data().begin_parse();
return ds~load_msg_addr();
}
slice parse_sender_address (cell in_msg_full) inline {
var cs = in_msg_full.begin_parse();
var flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
return sender_address;
}
() recv_internal (int balance, int msg_value, cell in_msg_full, slice in_msg_body) {
slice sender_address = parse_sender_address(in_msg_full);
slice owner_address = load_data();
if ~ equal_slices(sender_address, owner_address) {
}
}
Отправка сообщения
Итак, нам осталось наполнить тело условного оператора в соответствии с задачей, а именно отправить входящее сообщение.
Структура сообщения
С полной структурой сообщения можно ознакомиться здесь. Но обычно нам нет необходимости контролировать каждое поле, поэтому можно использовать краткую форму из примера:
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(message_body)
.end_cell();
Как вы можете видеть для построения сообщения используются функции стандартной библиотеки FunC. А именно фукнции примитивов Builder (частично построенных ячеек как вы можете помнить из первого урока). Рассмотрим:
begin_cell()
- создаст Builder для будущей ячейки
end_cell()
- создаст ячейку
store_uint
- сохранит uint в Builder
store_slice
- сохранит слайс в Builder
store_coins
- здесь в документации имеется ввиду store_grams
- используемой для записи количества Toncoin или других валют. Подробнее здесь.
А также дополнительно рассмотрим store_ref
, которая понадобится для отправки адреса.
store_ref
- Сохраняет ссылку на ячейку в Builder
Теперь когда у нас есть вся необходимая информация, сооберем сообщение.
Последний штрих - тело входящего сообщения
Чтобы отправить в сообщении тело, которое пришло в recv_internal
, соберем ячейку, а в самом сообщении сделаем на нее ссылку с помощью store_ref
.
if ~ equal_slices(sender_address, owner_address) {
cell msg_body_cell = begin_cell().store_slice(in_msg_body).end_cell();
}
Собираем сообщение
В соответствии с условием задачи мы должны отправить адрес и тело сообщения. А значит поменяем .store_slice(message_body)
на .store_slice(sender_address)
и .store_ref(msg_body_cell)
в переменной msg. Получим:
if ~ equal_slices(sender_address, owner_address) {
cell msg_body_cell = begin_cell().store_slice(in_msg_body).end_cell();
var msg = begin_cell()
.store_uint(0x10, 6)
.store_slice(owner_address)
.store_grams(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(sender_address)
.store_ref(msg_body_cell)
.end_cell();
}
Осталось только отправить наше сообщение.
Режим отправки сообщения (mode)
Для отправки сообщений используется send_raw_message
из стандартной библиотеки.
Переменную msg мы уже собрали, остается разобраться mode
. Описание каждого режиме есть в документации. Мы же рассмотрим на примере, чтобы было понятнее.
Пускай на балансе смарт-контракта 100 монет и мы получаем internal message c 60 моентами и отсылаем сообщение с 10, общий размер комиссий пусть будет для примера равен 3.
mode = 0
- баланс 100+60-10 = 150 монет, отправим 10-3 = 7 монет
mode = 1
- баланс 100+60-10-3 = 147 монет, отправим 10 монет
mode = 64
- баланс 100-10 = 90 монет, отправим 60+10-3 = 67 монет
mode = 65
- баланс 100-10-3 = 87 монет, отправим 60+10 = 70 монет
mode = 128
- баланс 0 монет, отправим 100+60-3 = 157 монет
Режимы 1 и 65 описанные выше это mode' = mode + 1
.
Так как по условию задачи, значение Toncoin, прикрепленное к сообщению, должно быть равно значению входящего сообщения за вычетом сборов, связанных с обработкой, нам подойдет режим mode = 64
с .store_grams(0)
. На примере получиться следующее:
Пусть на балансе смарт-контракта 100 монет и мы получаем internal message c 60 монетами и отсылаем сообщение с 0(так как .store_grams(0)
) общий fee 3.
mode = 64
- баланс (100 = 100 монет), отправим (60-3 = 57 монет)
Таким образом наш условный оператор будет выглядеть так:
if ~ equal_slices(sender_address, owner_address) {
cell msg_body_cell = begin_cell().store_slice(in_msg_body).end_cell();
var msg = begin_cell()
.store_uint(0x10, 6)
.store_slice(owner_address)
.store_grams(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(sender_address)
.store_ref(msg_body_cell)
.end_cell();
send_raw_message(msg, 64);
}
А полный код смарт-контракта:
#include "imports/stdlib.fc";
int equal_slices (slice a, slice b) asm "SDEQ";
slice load_data () inline {
var ds = get_data().begin_parse();
return ds~load_msg_addr();
}
slice parse_sender_address (cell in_msg_full) inline {
var cs = in_msg_full.begin_parse();
var flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
return sender_address;
}
() recv_internal (int balance, int msg_value, cell in_msg_full, slice in_msg_body) {
slice sender_address = parse_sender_address(in_msg_full);
slice owner_address = load_data();
if ~ equal_slices(sender_address, owner_address) {
cell msg_body_cell = begin_cell().store_slice(in_msg_body).end_cell();
var msg = begin_cell()
.store_uint(0x10, 6)
.store_slice(owner_address)
.store_grams(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(sender_address)
.store_ref(msg_body_cell)
.end_cell();
send_raw_message(msg, 64);
}
}
Обёртка на TypeScript
Для удобного взаимодействия с нашим смарт-контрактом, напишем обёртку на TypeScript. База для неё уже предоставляется от Blueprint.
Откроем файл wrappers/Proxy.ts
(название файла может быть другим, смотря как вы создавали проект).
Нам достаточно изменить лишь сборку ячейки данных контракта из конфига. Наш контракт содержит в свои данных единственное значение - адрес владельца. Добавим это значение в конфиг:
export type ProxyConfig = {
owner: Address;
};
export function proxyConfigToCell(config: ProxyConfig): Cell {
return beginCell().storeAddress(config.owner).endCell();
}
Отлично! Кроме данных нам ничего больше менять не нужно. смарт-контракт работает с любыми сообщениями и обёртку для них писать нам не надо.
Заключение
В этом уроке мы с вами реализовали простой прокси-контракт на FunC. Его тестированием мы займёмся в следующем уроке!
В качестве домашнего задания попробуйте задеплоить смарт-контракт в реальную сеть TON (можно и в тестнет) через скрипт, как мы уже делали в первом уроке и потом отправьте на него простые переводы с разными суммами и комментариями из своего кошелька.