Пишем Смарт-контракт на FunC для TON (The Open Network)

от автора

Введение

В этом уроке мы напишем ваш первый смарт-контракт в тестовой сети The Open Network на языке FUNC, задеплоим его в тестовую сеть с помощью toncli, а также протестируем его с помощью сообщения на языке Fift.

Требования

Для прохождения данного урока вам необходимо установить интерфейс для командной строки toncli

Смарт-контракт

Смарт-контракт, который мы будем делать, должен обладать следующей функциональностью:

  • хранить в своих данных целое число total — 64-битное число без знака;

  • при получении внутреннего входящего сообщения контракт, должен взять 32-битное целое число без знака из тела сообщения, добавить его к total и сохранить в данных контракта;

  • В смарт-контракте должен быть предусмотрен метод get total позволяющий вернуть значение total

  • Если тело входящего сообщения меньше 32 бит, то контракт должен выдать исключение

Создадим проект с помощью toncli

В консоли выполните следующие команды:

toncli start wallet cd wallet

Toncli создал простой проект кошелька, в нем вы можете увидеть 4 папки:

  • build;

  • func;

  • fift;

  • test;

На данном этапе нас интересуют папки func и fift, в которых мы будем писать код на FunС и Fift соответственно.

Что такое FunC и Fift

Высокоуровневый язык FunC используется для программирования смарт-контрактов на TON. Программы FunC компилируются в Fift ассемблерный код, который генерирует соответствующий байт-код для виртуальной машины TON(TVM). Далее этот байт-код (на самом деле дерево ячеек, как и любые другие данные в TON Blockchain) может быть использован для создания смарт-контракта в блокчейне или может быть запущен на локальном экземпляре TVM(Ton Virtual Machine).

Подробнее с FunC можно ознакомиться здесь

Подготовим файл для нашего кода

Зайдите в папку func:

cd func 

И откройте файл code.func, на своем экране вы уведите смарт-контракт кошелька, удалите весь код и мы готовы начать писать наш первый смарт контракт.

Внешние методы

У смарт-контрактов в сети TON есть два внешних метода к которым можно обращаться.

Первый, recv_external() эта функция выполняется когда запрос к контракту происходит из внешнего мира, то есть не из TON, например когда мы сами формируем сообщение и отправляем его через lite-client.

Второй, recv_internal() эта функция выполняется когда внутри самого TON, например когда какой-либо контракт обращается к нашему.

Под наши условия подходит recv_internal()

В файле code.func пропишем:

() recv_internal(slice in_msg_body) impure { ;; здесь будет код } 

;; две точки с запятой синтаксис однострочного комментария

Мы передаем в функцию слайс in_msg_body и используем ключевое слово impure

impure — ключевое слово, которое указывает на то, что функция изменяет данные смарт-контракта.

А вот чтобы понять, что такое слайс, поговорим про типы в смарт-контрактах сети TON

Типы cell,slice, builder, Integrer в FunC

В нашем просто смарт контракте мы будем использовать всего лишь четыре типа:

  • Cell(ячейка) — Ячейка TVM, состоящая из 1023 бит данных и до 4 ссылки на другие ячейки

  • Slice — Частичное представление ячейки TVM, используемой для разбора данных из ячейки

  • Builder — Частично построенная ячейка, содержащая до 1023 бит данных и до четырех ссылок; может использоваться для создания новых ячеек

  • Integrer — знаковое 257-разрядное целое число

Подробнее о типах в FunC:
кратко здесь
развернуто здесь в разделе 2.1

Преобразуем полученный слайс в Integer

Чтобы преобразовать полученный слайс в Integer добавим следующий код:
int n = in_msg_body~load_uint(32);

Функция recv_internal() теперь выглядит так:

() recv_internal(slice in_msg_body) impure { int n = in_msg_body~load_uint(32); } 

load_uint функция из стандартной библиотеки FunC она загружает целое число n-бит без знака из слайса.

Постоянные данные смарт-контракта

Чтобы добавить полученную переменную к total и сохранить значение в смарт-контракте, рассмотрим как реализован функционал хранения постоянных данных/хранилища в TON.

Примечание: не путайте с TON Storage, хранилище в предыдущем предложении удобная аналогия.

Виртуальная машина TVM является стековой, соответственно хорошей практикой хранения данных в контракте будет использовать определенный регистр, а не хранить данный «сверху» стека.

Для хранения постоянных данных отведен регистр с4, тип данных Ячейка.

Подробнее с регистрами можно ознакомиться с4 в пункте 1.3

Возьмем данные из с4

Для того чтобы «достать» данные из с4 нам понадобятся две функции из стандартной библиотеки FunC .

А именно:
get_data — берет ячейку из c4 регистра.
begin_parse — ячейку преобразует в slice

Передадим это значение в слайс ds

slice ds = get_data().begin_parse();

А также преобразуем этот слайс в Integer 64-бит для суммирования в соответствии с нашей задачей. (С помощью уже знакомой нам функции load_uint)

int total = ds~load_uint(64);

Теперь наша функция будет выглядеть так:

() recv_internal(slice in_msg_body) impure { int n = in_msg_body~load_uint(32);  slice ds = get_data().begin_parse(); int total = ds~load_uint(64); } 

Cуммируем

Для суммирования будем использовать бинарную операцию суммирования + и присвоение =

() recv_internal(slice in_msg_body) impure { int n = in_msg_body~load_uint(32);  slice ds = get_data().begin_parse(); int total = ds~load_uint(64);  total += n; }

Cохраняем значение

Для того чтобы сохранить постоянное значение, нам необходимо выполнить три действия:

  • создать Builder для будущей ячейки

  • записать в нее значение

  • из Builder создать Cell (ячейку)

  • Записать получившуюся ячейку в регистр

Делать это мы будем опять же с помощью функций стандартной библиотеки FunC

set_data(begin_cell().store_uint(total, 64).end_cell());

begin_cell() — создаст Builder для будущей ячейки
store_uint()— запишет значение total
end_cell()— создать Cell (ячейку)
set_data() — запишет ячейку в регистр с4

Итог:

() recv_internal(slice in_msg_body) impure { int n = in_msg_body~load_uint(32);  slice ds = get_data().begin_parse(); int total = ds~load_uint(64);  total += n;  set_data(begin_cell().store_uint(total, 64).end_cell()); } 

Генерация исключений

Все что осталось сделать в нашей internal функции это добавить вызов исключения, если получена переменная не 32-битная.

Для этого будем использовать встроенные исключения.

Исключения могут быть вызваны условными примитивами throw_if и throw_unless и безусловным throw .

Воспользуемся throw_if и передадим любой код ошибки. Для того, чтобы взять битность используем slice_bits().

throw_if(35,in_msg_body.slice_bits() < 32);

Вставим в начало функции:

() recv_internal(slice in_msg_body) impure { throw_if(35,in_msg_body.slice_bits() < 32);  int n = in_msg_body~load_uint(32);  slice ds = get_data().begin_parse(); int total = ds~load_uint(64);  total += n;  set_data(begin_cell().store_uint(total, 64).end_cell()); } 

Пишем Get функцию

Любая функция в FunC соответствует следующему паттерну:

[<forall declarator>] <return_type><function_name(<comma_separated_function_args>) <specifiers>

Напишем функцию get_total() возвращающую Integer и имеющую спецификацию method_id (об этом чуть позже)

int get_total() method_id { ;; здесь будет код } 

Method_id

Спецификация method_id позволяет вызывать GET функцию по названию из lite-client or ton-explorer.
Грубо говоря все фукнции в том имеют численный идентификатор, get методы нумеруются по crc16 хэшам их названий.

Берем данные из с4

Для того, что функция возвращала total хранящееся в контракте, нам надо взять данные из регистра, что мы уже делали:

int get_total() method_id { slice ds = get_data().begin_parse();  int total = ds~load_uint(64); return total; } 

Весь код нашего смарт-контракта

() recv_internal(slice in_msg_body) impure { throw_if(35,in_msg_body.slice_bits() < 32);  int n = in_msg_body~load_uint(32);  slice ds = get_data().begin_parse(); int total = ds~load_uint(64);  total += n;  set_data(begin_cell().store_uint(total, 64).end_cell()); }   int get_total() method_id { slice ds = get_data().begin_parse();  int total = ds~load_uint(64);  return total; } 

Деплоим контракт в тестовую сеть

Для деплоя в тестовую сеть будем использовать интерфейса для командной строки toncli

toncli deploy -n testnet

Что делать если пишет, что не хватает TON?

Необходимо получить их с тестового крана, бот для этого @testgiver_ton_bot
Адрес кошелька вы можете увидеть прямо в консоли, после команды деплой, toncli отобразит его справа от строки INFO: Found existing deploy-wallet

Чтобы проверить пришли ли TON на ваш кошелек в тестовой сети, можете использовать вот этот explorer: https://testnet.tonscan.org/

Важно: Речь идет только о тестовой сети

Тестируем контракт

Вызов recv_internal()

Для вызова recv_internal() необходимо послать сообщение внутри сети TON.
С помощью toncli send

Напишем небольшой скрипт на Fift, который будет отправлять 32-битное сообщение в наш контракт.

Скрипт сообщения

Для этого создадим в папке fift файл try.fif и напишем в нем следующий код:

"Asm.fif" include  <b 11 32 u, // number b> 

"Asm.fif" include — необходим для компиляции сообщения в байт код

Теперь рассмотрим сообщение:

<b b> — создают Builder ячейки, подробнее в пункте 5.2

5 32 u — кладем 32-битное unsigned integer 11

// number — однострочный комментарий

Деплоим получившееся сообщение

В командной строке:

toncli send -n testnet -a 0.03 --address "адрес вашего контракта" --body ./fift/try.fif

Теперь протестируем GET функцию:

toncli get get_total

Должно получиться следующее:

Код и остальные уроки можно найти тут.


ссылка на оригинал статьи https://habr.com/ru/post/673480/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *