📚Чтение заметок
На предыдущем уроке мы создали сценарий компиляции, и теперь пришло время начать писать код FunC. Знаешь, что круто? Теперь мы оснащены, чтобы увидеть, действительно ли работает наш код FunC. Как только мы напишем код FunC, мы просто запустим yarn compile и убедимся, что наш код может работать на TVM.
Смарт-контракт на этом уроке будет очень простым, но этого будет достаточно, чтобы ознакомиться с базовым синтаксом и структурой FunC.
Параметры, к которым мы получаем с помощью внутреннего обработчика сообщений recv_internal
Давайте возьмем код. В предыдущем уроке мы уже создали файл contracts/main.fc. Давайте откроем его и посмотрим, что у нас там есть:
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
}
Что у нас уже есть, так это функция, которая обрабатывает входящие сообщения. Как вы уже знаете из главы 1, любые транзакции на TON называются сообщениями.
Итак, что приносит нам сообщение? То, что мы уже видим, это три параметра, которые передаются в функцию recv_internal:
- Msg_value - этот параметр сообщает нам, сколько TON монет (или граммов) получено с этим сообщением
- In_msg - это полное сообщение, которое мы получили, со всей информацией о том, кто его отправил и т. д. Мы видим, что у него есть тип Cell. Что это значит? Тело сообщения хранится как ячейка на TVM, поэтому есть одна целая ячейка, посвященная нашему сообщению со всеми его данными.
- In_msg_body - это фактическая "читаемая" часть сообщения, которое мы получили. У него есть тип среза, потому что он является частью ячейки, он указывает "адрес", из какой части ячейки мы должны начать читать, если мы хотим прочитать этот параметр среза.
TODO: Уточните содержимое ячейки in_msg.
Как видите, как msg_value, так и in_msg_body являются производными от in_msg, но для удобства использования мы получаем их в качестве параметров в функцию receive_internal.
Спецификаторы функций
Я уверен, что вы заметили слово "impure" сразу после того, как параметры перешли в функцию. Это один из 3 возможных спецификаторов функций:
- impure
- Inline/inline_ref
- Method_id
- Один, несколько или ни один из них не может быть помещен в объявление функции, но в настоящее время они должны быть представлены в правильном порядке. Например, не разрешается ставить impure после встроенного.
На данный момент нас интересует только impure спецификатор, но мы будем покрывать остальные до тех пор, пока они начнут появляться в нашем коде.
Нечистый спецификатор означает, что функция может иметь некоторые побочные эффекты, которые нельзя игнорировать. Например, мы должны поставить impure спецификатор, если функция может изменять хранение контракта, отправлять сообщения или выдавать исключение, когда некоторые данные недействительны, и функция предназначена для проверки этих данных.
Если impure не указан и результат вызова функции не используется, то компилятор FunC может и удалит этот вызов функции.
Импорт stdlib.fc
Чтобы манипулировать данными и писать другую логику в нашем контракте, нам нужно сделать еще одну важную вещь. Нам нужно импортировать стандартную библиотеку FunC. В настоящее время эта библиотека является просто оберткой для наиболее распространенного ассемблера команд TVM, которые не являются встроенными. Описание каждой команды TVM, используемой в библиотеке, можно найти в документации.
Чтобы импортировать stdlib.fc, давайте создадим папку импорта внутри папки наших контрактов. Затем создайте файл stdlib.fc и заполните его содержимым официальной библиотеки stdlib.fc, которую вы можете получить here.
Теперь, в самом начале нашего main.fc, нам нужно вставить фактический импорт:
#include "imports/stdlib.fc";
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
}
Отлично, теперь мы готовы к работе!
Разбор in_msg
Давайте, наконец, узнаем, что мы можем сделать с параметрами, переданными нашей функции recv_internal.
Всякий раз, когда мы хотим обработать внутреннее сообщение, прежде чем мы даже начнем читать значимую часть in_msg_body, нам нужно сначала понять, какое внутреннее сообщение мы получили. В некоторых случаях могут быть разные случаи. Например, мы могли бы получить это сообщение, потому что наш контракт ранее отправил кому-то какое-то сообщение, и принимающая сторона не смогла его принять, поэтому оно "Bounced". Иногда мы не хотим обрабатывать такие сообщения. Мы поговорим о таком сценарии позже в этом курсе.
В каждом сообщении, которое мы получаем, есть флаги. Флаги в основном являются 4-битным целым числом, где каждый бит ... (TODO: Elaborate on structure of flags. int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool).
Эти флаги на самом деле сообщат нам ценную информацию о сообщении, такую как "Это сообщение было отозвано получателем, и теперь оно фактически возвращается".
Итак, первое, что должен сделать наш контракт, когда он получает внутреннее сообщение - разорать его. Давайте разораем in_msg:
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
}
Давайте разбьем, что именно происходит в этом коде:
Вам нужно будет запомнить эту концепцию, этот фрагмент - это "Address", указатель. Поэтому, когда мы разборим - мы разборим, начиная с какого-то места. В этом случае begin_parse() говорит нам, с чего мы должны начать разбор, он дает нам указатель на самый первый бит ячейки in_msg.
Затем мы разбираем 4-битное целое число, вызывая load_uint(4) и назначаем результат флагам переменной int.
Как только мы вызовем еще немного ~load_{*} на переменной cs, мы фактически продолжим разбор с места, где предыдущий ~load_{*} закончился.
В случае, если мы попытаемся разобрить что-то, чего на самом деле не существует в ячейке - наш контракт выйдет с кодом 9. Вы можете прочитать больше об ошибках стандартного кода here
В ячейке in_msg есть еще одна ценная информация, а именно - адрес отправителя, поэтому давайте продолжим разбор:
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr(); }
Когда мы хотим создать переменную, которая хранит адрес, мы всегда используем тип фрагмента, поэтому мы храним указатель только там, откуда память должна считывать адрес, как только это необходимо.
- Что мы будем делать с переменными, которые у нас уже есть? Позвольте мне сначала познакомить вас с еще 2 возможностями смарт-контракта:
1. Наш смарт-контракт имеет постоянное хранилище (называемое хранилищем c4)
2. Наш смарт-контракт может иметь метод геттера, который позволит любому из-за пределов мира получить некоторые данные из нашего контракта.
3. Используя эти две новые возможности, мы можем сделать следующее. Мы можем сохранить адрес отправителя в нашем хранилище и создать метод getter, который будет возвращаться при его вызове.
Другими словами, наш геттер всегда возвращал адрес контракта, который отправил сообщение в наш контракт самое позднее.
Использование постоянного хранилища
Чтобы хранить одни и те же данные в нашем постоянном хранилище c4, мы будем использовать стандартную функцию FunC set_data. Эта функция принимает и сохраняет ячейку.
Если мы хотим хранить больше данных, а затем вписываться в ячейку, мы можем легко написать "ref" на другую ячейку внутри первой. Такая ссылка называется ссылка. Мы можем записать до 4 ссылок в ячейку.
Давайте обновим наш код с помощью функции set_data и узнаем, как мы передаем в него ячейку.
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr();
set_data(begin_cell().end_cell()); }
Чтобы передать ячейку в set_data, или нам нужно сначала построить ее. Это легко сделать с помощью двух функций begin_cell() и end_cell().
На данный момент мы передаем пустую ячейку, это технически нормально, но мы хотим записать адрес отправителя сообщения в хранилище, поэтому мы должны обновить нашу ячейку с его помощью:
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr();
set_data(begin_cell().store_slice(sender_address).end_cell()); }
Для этой цели мы используем метод .store_slice().
Вуаля, у нас есть смарт-контракт, который может записать адрес отправителя в постоянное хранилище! Каждый раз, когда наш контракт будет получать внутреннее сообщение, он заменяет ячейку, хранящуюся в c4, новой ячейкой, которая будет иметь новый адрес отправителя. Так просто.
Использование методов геттера
Как вы помните, мы не хотели просто хранить адрес отправителя. Мы хотели, чтобы кто-нибудь мог прочитать последний адрес отправителя. Чтобы получить доступ к таким данным снаружи TVM, наш контракт должен иметь специальную функцию.
В последнее время мы говорили о спецификаторах функций. Чтобы сделать наши данные доступными за пределами TVM, мы создадим функцию и используем спецификатор method_id. Если функция имеет этот набор спецификаторов - то ее можно вызвать в lite-client или ton-explorer как метод получения по имени.
Давайте создадим один:
slice get_the_latest_sender() method_id {
}
Функции Getter размещаются рядом с функцией recv_internal
Как видите, мы определяем, какое время должно быть возвращено этой функцией, имя функции и спецификатор method_id.
Теперь давайте напишем логику чтения данных из постоянного хранилища и возврата их значения. Для этого мы используем стандартную функцию FunC get_data:
slice get_the_latest_sender() method_id {
slice ds = get_data().begin_parse();
return ds~load_msg_addr();
}
Как видите, мы снова используем begin_parse(), чтобы получить указатель, из которого мы собираемся разобрать ячейку, хранящуюся в хранилище c4.
Для загрузки сохраненного адреса мы используем ~load_msg_addr для загрузки адреса.
Составление нашего контракта
Наш окончательный код выглядит так:
#include "imports/stdlib.fc";
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
set_data(begin_cell().store_slice(sender_address).end_cell());
}
slice get_the_latest_sender() method_id {
slice ds = get_data().begin_parse();
return ds~load_msg_addr();
}
Довольно простой контракт, но мы многому научились, закодируя его, не так ли?
Поскольку мы написали весь запланированный код, давайте запустим прекрасную компиляцию пряжи в нашем терминале.
Если вы сделали все шаг за шагом со мной, вы должны увидеть следующий результат в терминале:
=================================================================
Compile script is running, let's find some FunC code to compile...
- Compilation successful!
- Compiled code saved to build/main.compiled.json
✨ Done in 1.40s.
Давайте проверим build/main.compiled.json, и мы увидим, что его содержимое изменилось - на этот раз шестнадцестное значение намного длиннее :) Это потому, что вы написали свой первый код FunC!
Мои поздравления!