Перейти к основному содержимому

Типы контрактов кошелька

warning

Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.

Возможно, вы слышали о различных версиях кошельков на блокчейне TON. Но что на самом деле означают эти версии и чем отличаются?

В этой статье мы рассмотрим различные версии и модификации кошельков TON.

к сведению

Before we start, there are some terms and concepts that you should be familiar with to fully understand the article:

  • Управление сообщениями – поскольку это основная функциональность кошельков.
  • Язык FunC – потому что мы будем в значительной степени полагаться на реализации, сделанные с его помощью.

Общая концепция

Чтобы разрушить напряжение, для начала мы должны понять, что кошельки не являются особой сущностью в экосистеме TON. Они по-прежнему являются всего лишь смарт-контрактами, состоящими из кода и данных, и в этом смысле равны любому другому субъекту (т.е. смарт-контракту) в TON.

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

Вы можете рассматривать каждую версию кошелька как реализацию смарт-контракта, предоставляющую стандартный внешний интерфейс, позволяющий различным внешним клиентам взаимодействовать с кошельками одинаковым образом. Вы можете найти эти реализации на языках FunC и Fift в основном репозитарии TON:

Базовые кошельки

Кошелек V1

Это самый простой вариант кошелька. Он позволяет вам отправлять только четыре транзакции одновременно и не проверяет ничего, кроме вашей подписи и seqno.

Исходный код кошелька:

Эта версия кошелька даже не используется в обычных приложениях, потому что у нее есть серьезные проблемы:

  • Нет простого способа получить seqno и публичный ключ из контракта.
  • Нет проверки valid_until, поэтому вы не можете быть уверены, что транзакция не будет подтверждена слишком поздно.

Первая проблема была исправлена в V1R2 и V1R3. Символ R означает "ревизия". Обычно ревизии – это небольшие обновления, которые добавляют только GET-методы (вы можете найти историю изменений в файле new-wallet.fif). Здесь и далее мы будем рассматривать только последние ревизии.

Тем не менее, поскольку каждая последующая версия наследует функциональность предыдущей, нам все равно следует придерживаться ее, поскольку это поможет нам в последующих версиях.

Расположение постоянной памяти

  • seqno: 32-битный порядковый номер.
  • public-key: 256-битный публичный ключ.

Оформление тела внешнего сообщения

  1. Данные:
  • подпись: 512-битная подпись ed25519.
  • msg-seqno: 32-битный порядковый номер.
  • (0-4)mode: до четырех 8-битных целых чисел, определяющих режим отправки для каждого сообщения.
  1. До 4 ссылок на ячейки, содержащие сообщения.

Как вы можете видеть, основная функциональность кошелька заключается в обеспечении безопасного способа связи с блокчейном TON из внешнего мира. Механизм seqno защищает от атак повторного воспроизведения, а подпись Ed25519 обеспечивает авторизованный доступ к функционалу кошелька. Мы не будем подробно останавливаться на каждом из этих механизмов, поскольку они подробно описаны на странице документации external message и довольно часто встречаются среди смарт-контрактов, получающих внешние сообщения. Данные полезной нагрузки состоят из до 4 ссылок на ячейки и соответствующего количества режимов, которые будут непосредственно переданы в метод send_raw_message(cell msg, int mode).

предупреждение

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

Коды завершения

Код завершенияОписание
0x21Проверка seqno не выполнена, сработала защита ответа
0x22Проверка Ed25519 signature не выполнена
0x0Стандартный код завершения при успешном выполнении
к сведению

Обратите внимание, что у TVM есть стандартные коды завершения (0x0 – один из них), поэтому вы также можете получить один из них, например, если у вас закончился газ, то вы получите код 0xD.

GET-методы

  1. int seqno() – возвращает текущий сохраненный seqno.
  2. int get_public_key – возвращает текущий сохраненный публичный ключ.

Кошелек V2

Исходный код кошелька:

В данной версии появился параметр valid_until, который используется для установки временного ограничения для транзакции, если вы не хотите, чтобы она была подтверждена слишком поздно. В этой версии также отсутствует get-метод для публичного ключа, который был добавлен в V2R2.

Все отличия по сравнению с предыдущей версией являются следствием добавления функциональности valid_until. Был добавлен новый код завершения: 0x23, отмечающий неудачу при проверке valid_until. Кроме того, в макет тела внешнего сообщения было добавлено новое поле Unix-time, задающее временной лимит транзакции. Все GET-методы остаются прежними.

Содержимое тела внешнего сообщения

  1. Данные:
  • signature: 512-битная подпись ed25519.
  • msg-seqno: 32-битный порядковый номер.
  • valid-until: 32-битное целое число Unix-time.
  • (0-4)mode: до четырех 8-битных целых чисел, определяющих режим отправки для каждого сообщения.
  1. До 4 ссылок на ячейки, содержащие сообщения.

Кошелек V3

В этой версии появился параметр subwallet_id, который позволяет вам создавать несколько кошельков, используя один и тот же публичный ключ (таким образом, у вас может быть только одна seed-фраза и несколько кошельков). Как и раньше, V3R2 добавляет только метод get-метод для публичного ключа.

Исходный код кошелька:

По сути, subwallet_id – это просто число, добавляемое к состоянию контракта при его развертывании. Поскольку адрес контракта в TON представляет собой хэш его состояния и код, адрес кошелька изменится на другой subwallet_id. Эта версия наиболее широко используется в настоящее время. Она охватывает большинство случаев использования и остается универсальной, простой и в основном такой же, как и предыдущие версии. Все GET-методы остаются прежними.

Содержимое постоянной памяти

  • seqno: 32-битный порядковый номер.
  • subwallet: 32-битный идентификатор subwallet.
  • public-key: 256-битный публичный ключ.

Содержимое внешнего сообщения

  1. Данные:
  • подпись: 512-битная подпись ed25519.
  • subwallet-id: 32-битный идентификатор subwallet.
  • msg-seqno: 32-битный порядковый номер.
  • valid-until: 32-битное целое число Unix-time.
  • (0-4)mode: до четырех 8-битных целых чисел, определяющих режим отправки для каждого сообщения.
  1. До 4 ссылок на ячейки, содержащие сообщения.

Коды завершения

Код завершенияОписание
0x23Проверка valid_until не выполнена; попытка подтверждения транзакции была предпринята слишком поздно
0x23Проверка Ed25519 signature не выполнена
0x21Проверка seqno не выполнена, сработала защита ответа
0x22subwallet-id не совпадает с сохраненным
0x0Стандартный код завершения при успешном выполнении

Кошелек V4

Эта версия сохраняет всю функциональность предыдущих версий, но также вводит нечто очень мощное: плагины (plugins).

Исходный код кошелька:

Эта функция позволяет разработчикам реализовать сложную логику, которая работает в тандеме с кошельком пользователя. Например, DApp может потребовать от пользователя платить небольшое количество монет каждый день, чтобы пользоваться определенными функциями. В этом случае пользователю нужно будет установить плагин на свой кошелек, подписав транзакцию. Затем плагин будет ежедневно отправлять монеты на адрес назначения по запросу из внешнего сообщения.

Плагины

Плагины – это, по сути, другие смарт-контракты на TON, которые разработчики могут реализовывать по своему усмотрению. По отношению к кошельку они представляют собой просто адреса смарт-контрактов, хранящиеся в словаре в постоянной памяти кошелька. Этим плагинам разрешено запрашивать средства и удалять себя из "разрешенного списка", отправляя внутренние сообщения кошельку.

Содержимое постоянной памяти

  • seqno: 32-битный порядковый номер.
  • subwallet-id: 32-битный идентификатор subwallet.
  • public-key: 256-битный публичный ключ.
  • plugins: словарь, содержащий плагины (может быть пустым).

Получение внутренних сообщений

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

  • op-code: 32-битный код операции. Это необязательное поле: любое сообщение, содержащее менее 32 бит в теле сообщения, либо неправильный oпкод или адрес отправителя, не зарегистрированный в качестве плагина, будет рассматриваться как простая отправка, аналогично предыдущим версиям кошелька.
  • query-id: 64-битное целое число. Это поле не влияет на поведение смарт-контракта, оно используется для отслеживания цепочек сообщений между контрактами.
  1. op-code = 0x706c7567, код операции запроса средств:
  • toncoins: VARUINT16 количество запрашиваемых toncoin.
  • extra_currencies: словарь, содержащий количество запрашиваемых дополнительных валют (может быть пустым).
  1. op-code = 0x64737472, запрос на удаление плагина-отправителя из "разрешенного списка".

Содержимое тела внешнего сообщения

  • signature: 512-битная подпись ed25519.
  • subwallet-id: 32-битный идентификатор subwallet.
  • valid-until: 32-битное целое число Unix-time.
  • msg-seqno: 32-битное длинное целое число последовательности.
  • op-code: 32-битный код операции.
  1. op-code = 0x0, простая отправка:
  • (0-4)mode: до четырех 8-битных целых чисел, определяющих режим отправки для каждого сообщения.
  • (0-4)сообщения:До четырех ссылок на ячейки, содержащие сообщения.
  1. op-code = 0x1, развертывание и установка плагина:
  • workchain: 8-битное целое число.
  • balance: VARUINT16 количество toncoin начального баланса.
  • state-init: ссылка на ячейку, содержащую начальное состояние плагина.
  • body: ссылка на ячейку, содержащую тело.
  1. op-code = 0x2/0x3, установка плагина/удаление плагина:
  • wc_n_address: 8-битный workchain_id + 256-битный адрес плагина.
  • balance: VARUINT16 сумма toncoin начального баланса.
  • query-id: 64-битное целое число.

Как вы можете видеть, четвертая версия по-прежнему предоставляет стандартную функциональность через оп-код 0x0, как и предыдущие версии. Оп-коды 0x2 и 0x3 позволяют производить манипуляции со словарем плагинов. Обратите внимание, что в случае с 0x2 вам необходимо самостоятельно развернуть плагин с этим адресом, в отличие от оп-кода 0x1, который также управляет процессом развертывания, но с помощью поля state_init.

подсказка

Коды завершения

Код завершенияОписание
0x24Проверка valid_until не выполнена, попытка подтверждения транзакции была предпринята слишком поздно
0x23Проверка Ed25519 signature не выполнена
0x21Проверка seqno не выполнена, сработала защита ответа
0x22subwallet-id не совпадает с сохраненным
0x27Не удалось выполнить манипуляции со словарем плагинов (0x1-0x3 recv_external op-codes)
0x50Недостаточно средств для запроса средств
0x0Стандартный код завершения при успешном выполнении

GET-методы

  1. int seqno() – возвращает текущий сохраненный seqno.
  2. int get_public_key() – возвращает текущий сохраненный публичный ключ.
  3. int get_subwallet_id() – возвращает идентификатор текущего subwallet.
  4. int is_plugin_installed (int wc, int addr_hash) – проверяет, установлен ли плагин с определенным воркчейн идентификатором и хэшем адреса.
  5. Кортеж get_plugin_list() – возвращает список плагинов.

Кошелек V5

На данный момент это самая современная версия кошелька, разработанная командой Tonkeeper, направленная на замену четвертой версии и допускающая произвольные расширения.




Стандарт пятой версии кошелька предлагает множество преимуществ, которые улучшают опыт как для пользователей, так и для продавцов. Данная версия поддерживает транзакции без газа, делегирование и восстановление аккаунта, оплату подписки с использованием токенов и toncoin, а также недорогие мультипереводы. Помимо сохранения предыдущей функциональности, новая версия контракта позволяет отправлять до 255 сообщений одновременно.

Исходный код кошелька:

Схема TL-B:

предупреждение

В отличие от предыдущих спецификаций версий кошельков, мы будем использовать схему TL-B из-за относительной сложности реализации интерфейса этой версии кошелька. Мы дадим некоторое описание каждого из них. Тем не менее, базовое понимание все равно необходимо, в сочетании с исходным кодом кошелька этого должно быть достаточно.

Содержимое постоянной памяти

contract_state$_
is_signature_allowed:(## 1)
seqno:#
wallet_id:(## 32)
public_key:(## 256)
extensions_dict:(HashmapE 256 int1) = ContractState;

Как вы видите, contractState по сравнению с предыдущими версиями, не претерпел серьезных изменений. Основным отличием является новый 1-битный флаг is_signature_allowed, который ограничивает или разрешает доступ через подпись и хранящийся публичный ключ. Мы опишем важность этого изменения в дальнейшем.

Процесс аутентификации

signed_request$_             // 32 (opcode from outer)
wallet_id: # // 32
valid_until: # // 32
msg_seqno: # // 32
inner: InnerRequest //
signature: bits512 // 512
= SignedRequest; // Total: 688 .. 976 + ^Cell

internal_signed#73696e74 signed:SignedRequest = InternalMsgBody;

internal_extension#6578746e
query_id:(## 64)
inner:InnerRequest = InternalMsgBody;

external_signed#7369676e signed:SignedRequest = ExternalMsgBody;

Прежде чем мы перейдем собственно к полезной нагрузке наших сообщений – InnerRequest, давайте сначала посмотрим, чем пятая версия отличается от предыдущих версий в части процесса аутентификации. Комбинатор InternalMsgBody описывает два варианта доступа к действиям кошелька через внутренние сообщения. Первый вариант нам уже знаком по версии 4: аутентификация в качестве ранее зарегистрированного расширения, адрес которого хранится в extensions_dict. Второй вариант – аутентификация с помощью хранящегося публичного ключа и подписи, аналогично внешним запросам.

Сначала это может показаться ненужной функцией, но на самом деле она позволяет обрабатывать запросы через внешние сервисы (смарт-контракты), которые не являются частью инфраструктуры расширения вашего кошелька – ключевая особенность пятой версии. Безгазовые транзакции полагаются на эту функциональность.

Обратите внимание, что простое получение средств по-прежнему является доступным. Практически же, любое полученное внутреннее сообщение, не прошедшее процесс аутентификации, будет считаться переводом.

Действия

Первое, на что следует обратить внимание, это InnerRequest, который мы уже видели в процессе аутентификации. В отличие от предыдущей версии, и внешние и внутренние сообщения имеют доступ к одной и той же функциональности, за исключением изменения режима подписи (т.е. флага is_signature_allowed).

out_list_empty$_ = OutList 0;
out_list$_ {n:#}
prev:^(OutList n)
action:OutAction = OutList (n + 1);

action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction;

// Extended actions in V5:
action_list_basic$_ {n:#} actions:^(OutList n) = ActionList n 0;
action_list_extended$_ {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1);

action_add_ext#02 addr:MsgAddressInt = ExtendedAction;
action_delete_ext#03 addr:MsgAddressInt = ExtendedAction;
action_set_signature_auth_allowed#04 allowed:(## 1) = ExtendedAction;

actions$_ out_actions:(Maybe OutList) has_other_actions:(## 1) {m:#} {n:#} other_actions:(ActionList n m) = InnerRequest;

Мы можем рассматривать InnerRequest как два списка действий:OutList – представляет собой необязательную цепочку ссылок на ячейки, каждая из которых содержит запрос на отправку сообщения, инициируемый режимом сообщения.ActionList – инициируется однобитным флагом, has_other_actions, который отмечает наличие расширенных действий, начиная с первой ячейки и далее в виде цепочки ссылок на ячейки. Мы уже знакомы с первыми двумя расширенными действиями, action_add_ext и action_delete_ext, за которыми следует внутренний адрес, который мы хотим добавить или удалить из словаря расширений. Третье действие, action_set_signature_auth_allowed, ограничивает или разрешает аутентификацию через публичный ключ, оставляя единственный способ взаимодействия с кошельком через расширения. Эта функциональность может оказаться чрезвычайно важной в случае потери или компрометации приватного ключа.

к сведению

Обратите внимание, что максимальное количество действий равно 255 – это следствие реализации через c5 TVM-регистр. Технически, вы можете сделать запрос с пустыми OutAction и ExtendedAction, но в таком случае это будет похоже на простое получение средств.

Коды завершения

Код завершенияОписание
0x84Попытка аутентификации через подпись, когда она отключена
0x85Проверка seqno не выполнена, сработала защита ответа
0x86wallet-id не соответствует сохраненному
0x87Проверка Ed25519 signature не выполнена
0x88Проверка valid-until не выполнена
0x89Убедитесь, что в send_mode установлен бит +2 (игнорировать ошибки) для внешнего сообщения.
0x8AПрефикс external-signed не соответствует полученному префиксу
0x8BОперация добавления расширения была неуспешной
0x8CОперация удаления расширения была неуспешной
0x8DНеподдерживаемый расширенный префикс сообщения
0x8EПопытка отключения аутентификации по подписи, пока словарь расширений пуст
0x8FПопытка установить подпись в уже установленное состояние
0x90Попытка удаления последнего расширения, когда подпись отключена
0x91Расширение имеет неправильный воркчейн
0x92Попытка изменения режима подписи с помощью внешнего сообщения
0x93Недопустимый c5, проверка action_send_msg не выполнена
0x0Стандартный код завершения при успешном выполнении
осторожно

Обратите внимание, что коды завершения 0x8E, 0x90 и 0x92 кошелька предназначены для того, чтобы вы не потеряли доступ к функциям кошелька. Тем не менее, вам следует помнить, что кошелек не проверяет, существуют ли сохраненные адреса расширений в TON на самом деле. Вы также можете развернуть кошелек с начальными данными, состоящими из пустого словаря расширений и ограниченного режима подписи. В этом случае вы сможете получить доступ к кошельку через публичный ключ до тех пор, пока не добавите свое первое расширение. Поэтому будьте осторожны с такими сценариями.

GET-методы

  1. int is_signature_allowed() – возвращает сохраненный is_signature_allowed флаг.
  2. int seqno() – возвращает текущий сохраненный seqno.
  3. int get_subwallet_id() – возвращает идентификатор текущего subwallet.
  4. int get_public_key() – возвращает текущий сохраненный публичный ключ.
  5. cell get_extensions() – возвращает словарь расширений.

Подготовка к безгазовым транзакциям

Как было сказано ранее, смарт-контракт кошелька v5 позволяет обрабатывать внутренние сообщения, подписанные владельцем. Это также позволяет совершать безгазовые транзакции, например, оплачивать сетевые комиссии при переводе USDt в сам USDt. Общая схема выглядит следующим образом:

image

подсказка

В следствие этого, будут существовать сервисы (такие как Tonkeeper's Battery), предоставляющие данную функциональность: они выплачивают комиссию за транзакцию в TON от имени пользователя, но взимают данную комиссию в токенах.

Пользовательский путь

  1. При отправке USDt пользователь подписывает одно сообщение, содержащее два исходящих перевода USDt:
  2. Перевод USDt на адрес получателя.
  3. Перевод небольшой суммы USDt в пользу Сервиса.
  4. Это подписанное сообщение отправляется off-chain по HTTPS на бэкэнд Сервиса. Бэкэнд Сервиса отправляет его в блокчейн TON, выплачивая toncoin за комиссию сети.

Бета-версия безгазового бэкэнда API доступна на tonapi.io/api-v2. Если вы разрабатываете какое-либо приложение для кошелька и у вас есть отзывы об этих методах, пожалуйста, поделитесь ими в чате @tonapitech.

Исходный код кошелька:

Специальные кошельки

Иногда функциональности базовых кошельков недостаточно. Поэтому существует несколько типов специализированных кошельков: high-load, lockup и restricted.

Давайте посмотрим на них.

Highload-кошельки

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

Lockup-кошелек

Если вам по какой-то причине нужно на некоторое время заблокировать монеты в кошельке без возможности их вывести до истечения этого времени, обратите внимание на lockup-кошелек.

Данный типа кошелька позволяет вам установить время, до которого вы не сможете ничего вывести из кошелька. Вы также можете настроить его, установив периоды разблокировки, чтобы получить возможность тратить монеты в течение этих периодов.

Например: вы можете создать кошелек, в котором будет храниться 1 миллион монет с общим сроком наделения правами 10 лет (total duration). Установите продолжительность блокировки (cliff_diration) на один год, таким образом, средства будут заблокированы в течение первого года после создания кошелька. Затем вы можете установить период разблокировки (unlock_period) в один месяц таким образом 1 000 000 TON / 120 месяцев = ~8333 TON будут разблокированы каждый месяц.

Исходный код кошелька:

Restricted-кошелек

Функциональность данного кошелька заключается в том, чтобы выступать в роли обычного кошелька, но ограничивать переводы только одним заранее определенным адресом назначения. Вы можете задать адрес назначения при создании этого кошелька, и тогда вы сможете переводить средства с него только на этот адрес. Но обратите внимание, что вы все еще можете переводить средства на контракты валидации, так что вы можете запустить валидатора с помощью этого кошелька.

Исходный код кошелька:

Известные оп-коды

Как видите, в TON существует множество различных версий кошельков. Но в большинстве случаев вам нужны только V3R2 или V4R2. Вы также можете использовать один из специальных кошельков, если вам нужна какая-то дополнительная функциональность, например, периодическая разблокировка средств.

См. также