📚Чтение Заметок
В этом уроке мы будем практиковаться в реализации более разных команд, создании функций и даже отправке сообщений внутри контракта. Мы собираемся сделать все это в дополнение к существующему контракту, который у нас уже есть.
Какой у нас план? Давайте разбьем это на:
- Наш контракт станет более строгим в отношении команд, мы собираемся ввести новый операционный код, предназначенный для логики внесения средств в контракт
- Еще один операционный код, который мы собираемся ввести, - это вывод средств. В процессе вывода средства средства фактически отправляются на адрес в виде сообщения, поэтому мы узнаем, как отправлять сообщения из контракта.
- Любые средства, которые прибудут с неизвестными операционными кодами, будут возвращены отправителю
- Мы введем новую стоимость в нашем хранилище - владелец контракта. Только владелец контракта может вывести средства. Мы также разделим нагрузку на хранилище и запишем логику в функции, чтобы сохранить наш основной код в чистоте.
Готовы? Установить. Иди!
Разделение логики управления хранилищем
Прежде всего, давайте разберемся с данными владельца контракта в хранилище (нам нужно иметь в виду, что нам придется поместить этот адрес в хранилище при инициировании контракта). Мы также создадим две новые функции - load_data и save_data:
(int, slice, slice) load_data() inline {
var ds = get_data().begin_parse();
return (
ds~load_uint(32), ;; counter_value ds~load_msg_addr(), ;; the most recent sender ds~load_msg_addr() ;; owner_address ); }
() save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {
set_data(begin_cell()
.store_uint(counter_value, 32) ;; counter_value .store_slice(recent_sender) ;; the most recent sender .store_slice(owner_address) ;; owner_address .end_cell()); }
Здесь есть несколько вещей, которые следует отметить:
- Встроенный спецификатор. Вы уже немного знаете о спецификаторах. Если функция имеет встроенный спецификатор, ее код фактически заменяется в каждом месте, где вызывается функция. Запрещается использовать рекурсивные вызовы в встроенных функциях.
- Почему load_data() не имеет нечистого спецификатора функции? Мы уже рассмотрели это в главе 3, но давайте подведем итоги. Ответ заключается в том, что эта функция не влияет на состояние контракта. Это только для чтения.
Вот как мы собираемся использовать эти функции далее в нашем коде:
var (counter_value, recent_sender, owner_address) = load_data();
save_data(counter_value,recent_sender,owner_address);
Нам не всегда нужно читать все параметры, но мы должны убедиться, что мы записываем их все обратно, потому что ячейка полностью обновляется после записи данных.
Новые коды
Давайте посмотрим, как наши функции управления хранилищем теперь размещаются в коде, а также введем больше операционных кодов, а также выпустим ошибку, если операционный код неизвестен в нашем контракте:
Несколько вещей, которые следует отметить:
- Мы также обновили функцию get_the_latest_sender, чтобы вернуть адрес владельца
- Если операционный код, прочитанный из тела сообщения, не вызвал ни одного из операторов if - мы выдаем ошибку с кодом 777. Вы можете выбрать этот номер, но убедитесь, что он не пересекается с official error exit codes. Exit_code выше 1, считается кодом ошибки, поэтому выход с таким кодом может привести к возврату/отскоку транзакции.
- Мы используем функцию return() для успешного выхода из исполнения контракта
В целом, код выглядит чистым, не так ли?
Внос и вывод средств
Нам очень легко на депозит (op == 2), так как в этом случае мы просто успешно заканчиваем исполнение, поэтому средства принимаются. В противном случае - средства будут возвращены отправителю.
if (op == 2) {
return();
}
- Тем не менее, с выводом средств становится немного сложнее. Мы должны сравнить адрес отправителя с адресом владельца смарт-контракта. Давайте посмотрим, что нам нужно знать, чтобы реализовать эту логику:
Для сравнения адресов владельца и отправителя мы используем стандартную функцию FunC equal_slice_bits()
Мы используем функцию throw_unless(), чтобы выбросить ошибку, если результат сравнения был ложным. Есть также другой способ пройти ошибку - throw_if(), этот выдает ошибку, если условие, переданное в эту функцию, возвращает true.
Тело сообщения с этой операцией также должно иметь целое число, указываюющее сумму, которую требуется вывести. Мы сравниваем эту сумму с фактическим балансом контракта (стандартная функция FunC get_balance())
Мы хотим, чтобы в нашем контракте всегда были какие-то средства, чтобы иметь возможность платить за аренду и необходимую информацию (узнать больше о сборах в главе 1), поэтому нам нужно будет установить какой-то минимум, который должен остаться в контракте и выбросить ошибку, если запрашиваемая сумма этого не позволяет.
Наконец, нам нужно узнать, как мы можем отправить монеты изнутри контракта через внутреннее сообщение
Давайте посмотрим, как все это работает вместе.
Сначала мы устанавливаем константу для минимального необходимого хранения:
const const::min_tons_for_storage = 10000000; ;; 0.01 TON
Затем мы реализуем логику вывода средств. Это будет выглядеть так:
if (op == 3) {
throw_unless(103, equal_slice_bits(sender_address, owner_address));
int withdraw_amount = in_msg_body~load_coins(); var [balance, _] = get_balance(); throw_unless(104, balance >= withdraw_amount);
int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);
;; TODO: Sending internal message with funds
return();
}
Как вы можете видеть, мы читаем количество монет, которые запрашиваем или сняты (это будет храниться в нашем in_msg_body сразу после кода операции, мы сделаем это в следующем уроке), проверяя, что баланс больше или равен запрошенной сумме вывода.
Мы также используем хорошую технику, чтобы убедиться, что минимальная сумма для хранения сохранена в контракте.
Давайте подробнее поговорим об этой логике, которая отправляет реальные средства.
Отправка внутреннего сообщения
Send_raw_message - это стандартная функция, которая принимает ячейку с сообщением и целым числом, содержащим сумму режима и флага. В настоящее время есть 3 режима и 3 флага для сообщений. Вы можете объединить один режим с несколькими (возможно, без) флагов, чтобы получить требуемый режим. Комбинация просто означает получение суммы их значений. Таблица с описаниями режимов и флагов приведена в this documentation part.
В нашем примере мы хотим отправить обычное сообщение и оплатить комиссию за перевод отдельно, поэтому мы используем режим 0 и флаг +1, чтобы получить режим = 1.
Ячейка сообщения, которую мы создаем перед передачей в send_raw_message(), вероятно, является самой продвинутой вещью, которую вам нужно будет понять до сих пор.
Мы потратим немного времени, чтобы убедиться, что эта часть станет для вас понятной. Но позвольте мне дать вам два совета, прежде чем мы начнем:
- Прежде всего, привыкни к тому, как данные хранятся в ячейках. Это называется сериализацией. На данный момент вам нужно будет познакомиться и привыкнуть к этому, так как вы будете довольно часто сериализировать данные в ячейку, так как все хранится в ячейках.
- Во-вторых, освойся с TON documentation. Это в значительной степени единственный способ обойти все эти структуры сериализации, так как их очень трудно запомнить.
Одна вещь, которую вы можете найти очень полезной, это old documentation portal, Так как в нем есть некоторые темы, разработанные очень простым и приятным образом.
Давайте разобьем структуру ячейки сообщения, которую мы собираемся передать в send_raw_message. Как вы уже поняли, нам нужно поместить несколько битов в ячейку, и есть определенная логика, какой бит отвечает за что.
Ячейка сообщения начинается с 1-битного префикса 0, затем есть три 1-битных флага, а именно: отключена ли мгновенная маршрутизация гиперкуба (в настоящее время всегда верно), должно ли сообщение быть возвращено, если есть ошибки во время его обработки, является ли само сообщение результатом отскока. Затем адреса источника и назначения сериализуются, за которым следует значение сообщения и четыре целых числа, связанные с оплатой и временем пересылки сообщений.
Если сообщение отправляется из смарт-контракта, некоторые из этих полей будут переписаны в правильные значения. В частности, валидатор будет переписывать bounced, src, ihr_fee, fwd_fee, created_lt и created_at. Это означает две вещи: во-первых, другой смарт-контракт во время обработки сообщения может доверять этим полям (отправитьель не может подделывать адрес источника, флаг отсказа и т. д.); и во-вторых, во время сериализации мы можем приписать в эти поля любые допустимые значения (в любом случае эти значения будут перезаписаны).
Простая сериализация сообщения будет следующей (взята из documentation portal):
Однако вместо пошаговой сериализации всех полей разработчики обычно используют ярлыки. Таким образом, давайте рассмотрим, как сообщения могут быть отправлены из смарт-контракта на примере:
Давайте подробнее посмотрим, что здесь происходит.
.Store_uint(0x18, 6) Мы начинаем составлять ячейку, помещая значение 0x18 в 6 бит, что составляет 0b011000, если мы переводим его из шестнадцатеричного. Что это? 0b011000Первый бит - это префикс 0—1 бит, который указывает на то, что это int_msg_info (информация о внутреннем сообщении).
Затем есть 3 бита 1, 1 и 0, что означает: Instant Hypercube Routing отключен (мы не будем вдаваться в подробности о том, что это такое, вещи слишком низкого уровня, которые вам не понадобятся при написании контрактов в данный момент)
Сообщения могут быть ототсканы
Сообщение не является результатом самого отскока.
Тогда должен быть адрес отправителя, однако, поскольку он в любом случае будет переписан (когда валидатор укажет здесь фактический адрес отправителя, как мы обсуждали выше), с тем же эффектом, любой действительный адрес может быть сохранен там. Самая короткая допустимая сериализация адреса - это серия addr_none, и она сериализуется как двухбитная строка 00.
Таким образом, .store_uint(0x18, 6) - это оптимизированный способ сериализации тега и первых 4 полей.
- .Store_slice(addr) - эта строка сериализирует адрес назначения.
- .Store_coins(граммы) - эта строка просто сериализует количество монет (граммы - это просто настоящая сумма с количеством монет)
- .Store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - это интересно. Технически мы просто записываем большое крепление нулей в ячейку (а именно, количество нулей равно 1 + 4 + 4 + 64 + 32 + 1 + 1). Почему так? Мы знаем, что существует четкая структура того, как эти значения потребляются, и это означает, что каждый 0, который мы ставим туда, по какой-то причине. Интересная часть заключается в том, что эти нули работают двумя способами - некоторые из них ставятся как 0, потому что валидатор все равно перепишет значение, некоторые из них ставятся как 0, потому что эта функция еще не поддерживается (например, дополнительные валюты). Просто чтобы убедиться, что мы понимаем, почему существует так много нулей, давайте разберем предполагаемую структуру: Первый бит означает пустой словарь дополнительных валют.
Затем у нас есть два 4-битных поля. Поскольку ihr_fee и fwd_fee будут перезаписаны, мы также можем поставить там нули.
Затем мы ставим ноль в поля created_lt и created_at. Эти поля также будут перезаписаны; однако, в отличие от сборов, эти поля имеют фиксированную длину и, таким образом, кодируются как 64- и 32-битные строки.
Следующий нулевой бит означает, что нет поля init.
Последний нулевой бит означает, что msg_body будет сериализован на месте. Это в основном указывает на то, есть ли msg_body с пользовательским макетом.
- .Store_uint(op_code, 32).store_uint(query_id, 64); эта часть самая простая - мы передаем тело сообщения с пользовательским макетом, что означает, что мы можем поместить туда любые данные, если получатель знает, как их обрабатывать.
Давайте посмотрим, как это относится к нашему коду вывода средств:
Завершение нашего кода контракта
И последнее, что нужно сделать, это добавить новый метод геттера, который возвращает остаток нашего контракта:
int balance() method_id {
var [balance, _] = get_balance();
return balance;
}
Давайте еще раз посмотрим, как выглядит наш окончательный код:
Давайте проверим, что наш код компилируется с помощью yarn compile, и приступим к написанию тестов для нашего обновленного контракта.