- Введение
- Базовая структура смарт-контракта
- Обработка входящих сообщений в смарт-контракте
- Содержимое ячейки с входящим сообщением (Cell in_msg_full)
- Содержимое тела сообщения (slice in_msg_body)
- Отправка исходящих сообщений в смарт-контракте
- Рекомендации по написанию смарт-контрактов
- Проверяйте наличие модификатора impure
- Проверяйте принадлежность адреса к workchain при обработке сообщений
- Всегда проверяйте модифицирующие/не модифицирующие методы
- Используйте нулевой адрес для сжигания токенов
- При желании используйте символьные обозначения для opcode
- Как работать со словарями (Hashmap or Dictionaries)?
- Хранение и извлечение словаря из храналища
- Получение данных из словаря и его очистка
- Hashmap с адресом в качестве ключа
Введение
Смарт-контакрты 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.
Последовательность от написания до публикации смарт-контракта выглядит следующим образом:
- Создается смарт-контракт на языке FunC,
- компилируется в Fift Assembler,
- при помощи Fift формируется файл .boc (“bag of cells”),
- публикуется в блокчейне.
Базовая структура смарт-контракта
После того как смарт-контракт опубликован, можно взаимодействовать с ним отправляя на него сообщения:
- внешнее – при помощи внешних клиентов, таких как 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 (); }
Тело сообщения обычно начинается со следующих полей:
- op — 32-bit (big-endian) unsigned integer, идентифицирующее операцию для исполнения, или метод смарт-контракта для вызова.
- 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); ;; идентификатор сообщения
- оставшаяся часть тела сообщения специфична для каждого поддерживаемого значения параметра 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 (); }
// импорт функций работы 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));
(статья будет дополняться…)