Telegram Open Network (TON): FunC рекомендации для создания смарт-контакртов в TON

Введение

Смарт-контакрты TON исполняются на TON Virtual Machine (TVM). Ввиду несовместимости с синхронным блокчейном исполняемым на EVM (Ethereum Virtual Machine), для разработки смарт-контрактов под TVM был разработан специальный высокоуровневый язык программирования FunC, который позволяет разработчику работать именно с акторной парадигмой, в отличие от Solidity. Программный код на FunC компилилуются в другой язык Fift Assembler и выполняться в TVM.

Информацию об особенностях TON читайте в статье “Погружение в блокчейн для разработков“.

Сообщения для смарт-контактов представляют из себя бинарные данные и создаются при помощи специально разработанных SDKs либо пишутся на специальном языке программирования Fift. Основная способность этого языка – это возможность напрямую взаимодействовать с TVM.

Таким образом, команда TON разработала целых 3 новых языка программирования: FunC, Fift и Fift Assembler.

Последовательность от написания до публикации смарт-контракта выглядит следующим образом:

  1. Создается смарт-контракт на языке FunC,
  2. компилируется в Fift Assembler,
  3. при помощи Fift формируется файл .boc (“bag of cells”),
  4. публикуется в блокчейне.

Базовая структура смарт-контракта

После того как смарт-контракт опубликован, можно взаимодействовать с ним отправляя на него сообщения:

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

Для обработки внутренних и внешних сообщений в смарт-контракте используются 2 функции:

;; для обычных транзакций, вызванных сообщением, начальное состояние стека (входящих параметров) выглядит следующим:
;; int my_balance - баланс смарт-контракта (в наноТонах)
;; int msg_value - баланс входящего сообщения (в наноТонах)
;; cell in_msg_full - ячейка с входящим сообщением
;; slice in_msg_body - тело (body) входящего сообщения
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    ;; TODO: implementation 
}

;; slice in_msg - тело сообщения
() recv_external(slice in_msg) impure {
    ;; TODO: implementation  
}

Из контекста их названия recv_internal принимает внутренние сообщения и эта функция является одной из обязательных (если не определена функция main), а recv_external принимает внешние сообщения (необязательная). Тип slice является срезом типа cell и используется в основном для ее парсинга. Т.е. на вход данных функций мы ожидаем всегда тип slice, чтобы иметь возможность распарсить входящие сообщения. Ключевое слово impure в функциях означает, что данные функции вносят изменения в данные смарт-контракта.

Обработка входящих сообщений в смарт-контракте

Содержимое ячейки с входящим сообщением (Cell in_msg_full)

При обработке большинства сообщений, сдесь нас больше всего интересуют: адрес отправителя сообщения (slice sender_address) и флаги (int flags), реже еще бывает необходимость достать значение fwd_fee для оценки стоимости forward_payload.

slice cs = in_msg_full.begin_parse();   ;; преобразуем cell в slice

int flags = cs~load_uint(4);            ;; загружаем флаги
if (flags & 1) {                        ;; если true, то значит мы получили bounced сообщение
    on_bounce(in_msg_body);             ;; обрабатываем данный вид сообщения если нужно
    return ();
}

slice sender_address = cs~load_msg_addr(); ;; адрес отправителя сообщения

;; следующий код позволяет доббраться до fwd_fee если он нам нужен
cs~load_msg_addr(); ;; skip dst
cs~load_coins(); ;; skip value
cs~skip_bits(1); ;; skip extracurrency collection
cs~load_coins(); ;; skip ihr_fee
int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; используем fwd_fee для оценки стоимости forward_payload

Содержимое тела сообщения (slice in_msg_body)

Тело сообщения может быть передано пустым, например, когда просто отправили из кошелька несколько TON на смарт-контракт.

Проверить можно так:

if (in_msg_body.slice_empty?()) { ;; игнорируем пустое сообщение
    return ();
}

Тело сообщения обычно начинается со следующих полей:

  1. op — 32-bit (big-endian) unsigned integer, идентифицирующее операцию для исполнения, или метод смарт-контракта для вызова.
  2. query_id — 64-bit (big-endian) unsigned integer, используемое во всех внутренних сообщениях типа вопрос-ответ, чтобы идентифицировать связь ответа с запросом (query_id ответа должен равняться query_id соответствующего запроса). Если op — не метод типа “запрос-ответ” (он вызывает метод, от которого не ожидается ответ), то query_id может быть упущен.

    Для парсинга этих двух сущностей, можно использовать следующий код:

    int op = in_msg_body~load_uint(32); ;; операция
    int query_id = in_msg_body~load_uint(64); ;; идентификатор сообщения
  3. оставшаяся часть тела сообщения специфична для каждого поддерживаемого значения параметра op.

Отправка исходящих сообщений в смарт-контракте

Сообщение в общем виде выглядит следующим образом:

cell msg = begin_cell()
  .store_uint(0x18, 6)    ;; 0х18 - bounced ("запрос-ответ"); 0х10 - non-bounced (только "запрос"); 0x30 - logs
  .store_slice(addr)      ;; адрес, на который отправляем сообщение
  .store_coins(amount)    ;; сумма TON которую переводим на контракт
  .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)  ;; параметры сообщения (обычно не меняются)
  .store_uint(op::excesses(), 32)  ;; тело сообщения (код операции)
  .store_uint(query_id, 64)        ;; тело сообщения (идентификатор сообщения)
.end_cell();

В примере выше показан вариант, когда тело сообщения вы передаете внутри Cell самого сообщения. Есть еще вариант использовать store_ref (ссылку на другую ячейку) – если тело сообщения у вас хранится в отдельной Cell.

Оратите внимание, что любая ячейка (cell) может содержать до 1023 бит. Если вам нужно хранить больше данных, вы должны разделить их на части и хранить в ссылочных ячейках.

Более подробную информацию о сообщениях можно узнать в статье “Отправка сообщений между смарт-контрактами

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

send_raw_message(msg.end_cell(), mode);

где значение mode в примере, это число, которое характеризует режим отрпавки.

Пускай на балансе смарт-контракта = 100 монет и мы получаем internal message c 60 монетами и отсылаем сообщение с 10, общая коммиссия (fee) = 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 монет).

Рекомендации по написанию смарт-контрактов

Проверяйте наличие модификатора impure

Он необходим для всех функций, которые меняю состояние контракта, отправляют сообщения или могут вызывать исключения.
Пример использования в функции авторизации:

() authorize (sender) impure inline {
  throw_unless(187, equal_slice_bits(sender, addr1) | equal_slice_bits(sender, addr2));
}

Проверяйте принадлежность адреса к workchain при обработке сообщений

slice to_owner_address = in_msg_body~load_msg_addr();  ;; адрес кошелька получателя
force_chain(to_owner_address);  ;; проверяет, что адрес находится в воркчейне с номером 0 (базовый ворчейн)


int workchain() asm "0 PUSHINT";

;; проверяет, что адрес находится в воркчейне с номером 0 (базовый ворчейн)
() force_chain(slice addr) impure {
  (int wc, _) = parse_std_addr(addr);     ;; возвращает из MsgAddressInt воркчейн и 256-битный integer адрес
  throw_unless(333, wc == workchain());   ;; вызов исключения, если воркчейны не равны
}

Всегда проверяйте модифицирующие/не модифицирующие методы

Когда вызываете методы, поверяйте, правильно ли вы их вызываете. Вызов через символ . не модифицирует сущность, а вызов через ~ модифицирует.

slice ds = get_data().begin_parse();
int d1 = ds~load_uint(64);	// вернет из ds значение и удалит его в самом ds, сместив курсор дальше
int d2 = ds.preload_uint(64);	// вернет из ds значение, но в самом ds целостность данных не поменяется

Используйте нулевой адрес для сжигания токенов

Иногда в смарт-контрактах возникает необходимость использования нулевого адреса для сжигания токенов. В блокчейне TON он выглядит так:

const slice dummy_address = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"a;

При желании используйте символьные обозначения для opcode

Речь про ваши собственные опкоды, которые вы добавляете в логику смарт-контракта.

Им можно придавать смысловые обозначения, пример:

int op = in_msg_body~load_uint(32); 
if(op == "deposit"c) {  ;; здесь после смыслового названия, после кавычек нужно указать символ "c" (обозначает формат crc32)
    return ();
}
Пример, как для данного формата формировать опкоды через TypeScript
// импорт функций работы crc32
import { crc32 } from "./crc32";

// так формируем константу опкода при помощи конвертации строки с названием опкода в формат crc32
export const Opcodes = {
    deposit: crc32("deposit")
}

// так указываем в билдере опкод для отправки сообщения
.storeUint(Opcodes.deposit, 32)


// Код файла "crc32":
const POLYNOMIAL = -306674912;

let crc32_table: Int32Array | undefined = undefined;

export function crc32(str: string, crc = 0xFFFFFFFF) {
    let bytes = Buffer.from(str);
    if (crc32_table === undefined) {
        calcTable();
    }
    for (let i = 0; i < bytes.length; ++i)
        crc = crc32_table![(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
    return (crc ^ -1) >>> 0;
}

function calcTable() {
    crc32_table = new Int32Array(256);
    for (let i = 0; i < 256; i++) {
        let r = i;
        for (let bit = 8; bit > 0; --bit)
            r = ((r & 1) ? ((r >>> 1) ^ POLYNOMIAL) : (r >>> 1));
        crc32_table[i] = r;
    }
}

Как работать со словарями (Hashmap or Dictionaries)?

Ключами словаря – могут быть unsigned/signed integer или Slice.
Значениями словаря – могут быть Slice или Cell.

Хранение и извлечение словаря из храналища

Вместо … нужно подставить другие ваши аргументы, которые вам необходимы в хранилище.

(..., cell) load_data() inline {
    slice ds = get_data().begin_parse();
    return (
        ...,
        (ds.slice_bits() > 0 ? ds~load_dict() : new_dict())
    );
}

() save_data(..., cell dict) impure {
    set_data(
        begin_cell()
            ...
            .store_dict(dict)
        .end_cell()
    );
}

Получение данных из словаря и его очистка

В примере ниже обход словаря в цикле происходит от максимального значения к минимальному.

;; метод очищает словарь по условию
;; cell dict - словарь
;; int total - количество элементов в словаре
cell clean_dictionary(cell dict, int total) {
    ;; начальное значение с которого будем делать обход
    int pivot = total - 10;	;; не трогаем 10 последних значений
    do {
        ;; достаем ключи от max к min
        (pivot, slice value, int found) = dict.udict_get_prev?(256, pivot);
        
        if (found) {
            dict~udict_delete_get?(256, pivot);
        }
    
    } until ( ~ found) ;; до тех пор пока найден, выполняем действие
    
    return dict;
}

;; get-метод, возвращает словарь
var get_addrs_list() method_id {
    (_, cell dict) = load_data();
    
    var l = null();		;; пустой список
    int pivot = 100;	;; начальное значение
    
    do {
        ;; достаем ключи от max к min
        (pivot, slice value, int found) = dict.udict_get_prev?(256, pivot);
        
        if (found) {
            slice addr = value~load_msg_addr();  ;; достаем адрес из value словаря
            l = cons(addr, l);  ;; кладем адрес в список
        }
    } until ( ~ found)
    
    return l;
}

Если нужен обход словаря от минимального к максимальному индексу

;; если нужно наоборот, от min к max (в этом случае устанавливаем int pivot = -1)
(pivot, slice value, int found) = dict.udict_get_next?(256, pivot);

Как можно быстро проверить, есть ли ключ в словаре и вернуть исключение если он не найден?

int id = cell_hash(proposal);  ;; в качестве ID используется hash ячейки
(_, int found?) = proposals.udict_get?(256, id);  ;; поиск по ключу
throw_if(105, found?); ;; выкидываем исключение, если ключ найден

Hashmap с адресом в качестве ключа

Как в словаре хранить значение, где ключом key должен быть адрес? Для этого нужно через вспомогательный метод получить адрес в виде 256-bit int

;; получаем адрес в виде 256-bit int и проверяем воркчейн
(int wc, int sender) = parse_std_addr(sender_address);
throw_unless(99, wc == 0);

;; находим и удаляем из словаря значение по ключу
(_, slice old_balance_slice, int found?) = accounts~udict_delete_get?(256, sender);

;; если нужно просто выбить исключение если ключ не найден
;; throw_unless(98, found?);

;; если найдено, обновляем баланс
if(found?) {
    balance += old_balance_slice~load_coins();
}

;; добавляем значение в словарь с новым балансом
accounts~udict_set_builder(256, sender, begin_cell().store_coins(balance));

(статья будет дополняться…)

 

Рейтинг
( 2 оценки, среднее 5 из 5 )
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: