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

Контракты кошельков

Возможно, вы слышали о разных версиях кошельков на блокчейне 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:

Стандартные кошельки

Хеши контрактов кошельков

Здесь вы можете найти текущие хеши различных версий кода контрактов кошельков. Подробные спецификации каждого контракта кошелька приведены ниже в тексте, а также на странице ContractSources.md.

Показать таблицу хешей контрактов кошельков
Версия контрактаХеш
walletv1r1oM/CxIruFqJx8s/AtzgtgXVs7LEBfQd/qqs7tgL2how=
walletv1r21JAvzJ+tdGmPqONTIgpo2g3PcuMryy657gQhfBfTBiw=
walletv1r3WHzHie/xyE9G7DeX5F/ICaFP9a4k8eDHpqmcydyQYf8=
walletv2r1XJpeaMEI4YchoHxC+ZVr+zmtd+xtYktgxXbsiO7mUyk=
walletv2r2/pUw0yQ4Uwg+8u8LTCkIwKv2+hwx6iQ6rKpb+MfXU/E=
walletv3r1thBBpYp5gLlG6PueGY48kE0keZ/6NldOpCUcQaVm9YE=
walletv3r2hNr6RJ+Ypph3ibojI1gHK8D3bcRSQAKl0JGLmnXS1Zk=
walletv4r1ZN1UgFUixb6KnbWc6gEFzPDQh4bKeb64y3nogKjXMi0=
walletv4r2/rX/aCDi/w2Ug+fg1iyBfYRniftK5YDIeIZtlZ2r1cA=
walletv5r1IINLe3KxEhR+Gy+0V7hOdNGjDwT3N9T2KmaOlVLSty8=

Примечание: Эти хеши также можно найти в эксплорерах.

Кошелёк V1

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

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

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

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

Первая проблема была исправлена в версиях V1R2 и V1R3. Буква R означает ревизию. Обычно ревизии — это небольшие обновления, которые добавляют только get-методы; вы можете найти их в истории изменений new-wallet.fif. Здесь и далее мы будем рассматривать только последние ревизии.

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

Официальные хеши кода

Версия контрактаХеш
walletv1r1oM/CxIruFqJx8s/AtzgtgXVs7LEBfQd/qqs7tgL2how=
walletv1r21JAvzJ+tdGmPqONTIgpo2g3PcuMryy657gQhfBfTBiw=
walletv1r3WHzHie/xyE9G7DeX5F/ICaFP9a4k8eDHpqmcydyQYf8=

Структура постоянной памяти

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

Структура тела внешнего сообщения

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

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

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

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

Коды возврата

Код возвратаОписание
0x21Проверка seqno не пройдена, сработала защита от повторного воспроизведения
0x22Проверка подписи Ed25519 не пройдена
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-времени, устанавливающее ограничение по времени для транзакции. Все get-методы остались прежними.

Официальные хеши кода

Версия контрактаHash
walletv2r1XJpeaMEI4YchoHxC+ZVr+zmtd+xtYktgxXbsiO7mUyk=
walletv2r2/pUw0yQ4Uwg+8u8LTCkIwKv2+hwx6iQ6rKpb+MfXU/E=

Структура тела внешнего сообщения

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

Кошелёк V3

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

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

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

Официальные хеши кода

Версия контрактаHash
walletv3r1thBBpYp5gLlG6PueGY48kE0keZ/6NldOpCUcQaVm9YE=
walletv3r2hNr6RJ+Ypph3ibojI1gHK8D3bcRSQAKl0JGLmnXS1Zk=

Структура постоянной памяти

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

Структура тела внешнего сообщения

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

Коды возврата

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

Кошелёк V4

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

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

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

Официальные хеши кода

Версия контрактаHash
walletv4r1ZN1UgFUixb6KnbWc6gEFzPDQh4bKeb64y3nogKjXMi0=
walletv4r2/rX/aCDi/w2Ug+fg1iyBfYRniftK5YDIeIZtlZ2r1cA=

Плагины

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

Структура постоянной памяти

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

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

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

  • opcode?opcode?: 32-битный код операции. Это необязательное поле. Это необязательное поле. Любое сообщение, содержащее менее 32 бит в теле сообщения, неверный код операции или адрес отправителя, который не зарегистрирован как плагин, будет считаться простой передачей средств, как в предыдущих версиях кошельков.
  • query-id: 64-битное целое число. Это поле не влияет на поведение смарт-контракта; оно используется для отслеживания цепочек сообщений между контрактами.
  1. opcode = 0x706c7567, код операции запроса средств.
    • toncoins: VARUINT16 с количеством запрошенных Toncoin.
    • extra_currencies: словарь, содержащий количество запрошенных «дополнительных валют» (extra currencies), может быть пустым.
  2. opcode = 0x64737472, запрос на удаление плагина-отправителя из «списка разрешённых».

Структура тела внешнего сообщения

  • signature: 512-битная подпись Ed25519.
  • subwallet-id: 32-битный идентификатор субкошелька.
  • valid-until: 32-битное целое число с отметкой UNIX-времени.
  • msg-seqno: 32-битный порядковый номер.
  • opcode: 32-битный код операции.
  1. opcode = 0x0, простая отправка.
    • (0-4) mode: до четырёх 8-битных целых чисел, определяющих режим отправки для каждого сообщения.
    • (0-4)messages: до четырёх ссылок на ячейки, содержащие сообщения.
  2. opcode = 0x1, развёртывание и установка плагина.
    • workchain: 8-битное целое число.
    • balance: VARUINT16 с количеством Toncoin начального баланса.
    • state_init: ссылка на ячейку, содержащую начальное состояние плагина.
    • body: ссылка на ячейку, содержащую тело.
  3. opcode = 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 не пройдена
0x21Проверка seqno не пройдена, сработала защита от повторного воспроизведения
0x22subwallet_id не соответствует сохранённому
0x27Операция со словарём плагинов завершилась неудачно (коды операций recv_external 0x1-0x3)
0x50Недостаточно средств для запроса средств
0x0Стандартный код выхода при успешном выполнении.

Get-методы

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

Кошелёк V5

Это самая современная на данный момент версия кошелька, разработанная командой Tonkeeper, призванная заменить V4 и поддерживающая произвольные расширения.


Диаграмма структуры взаимодействия с контрактом кошелька V5Диаграмма структуры взаимодействия с контрактом кошелька V5

Стандарт кошелька V5 предлагает множество преимуществ, которые улучшают опыт как для пользователей, так и для продавцов. V5 поддерживает транзакции «без газа», делегирование и восстановление аккаунтов, платежи по подписке с использованием токенов и Toncoin, а также недорогие мульти-переводы. В дополнение к предыдущей функциональности (V4), новый контракт позволяет отправлять до 255 сообщений за раз.

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

Схема TL-B:

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

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

Официальный хеш кода

Версия контрактаHash
walletv5r1IINLe3KxEhR+Gy+0V7hOdNGjDwT3N9T2KmaOlVLSty8=

Структура постоянной памяти

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 — давайте сначала посмотрим, чем версия 5 отличается от предыдущих в отношении аутентификации. Комбинатор InternalMsgBody описывает два способа доступа к действиям кошелька через внутренние сообщения. Первый метод нам уже знаком по версии 4: аутентификация в качестве ранее зарегистрированного расширения, адрес которого хранится в extensions_dict. Второй метод — это аутентификация через хранимый публичный ключ и подпись, что схоже с внешними сообщениями.

Сначала это может показаться ненужной функцией, но на самом деле она позволяет обрабатывать запросы через внешние сервисы (смарт-контракты), не входящие в число расширений вашего кошелька — это ключевая особенность V5. Транзакции «без газа» основаны на этой функциональности.

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

Действия

Первое, на что мы должны обратить внимание — 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; это следствие реализации с помощью регистра TVM c5. Технически вы можете сделать запрос с пустыми OutAction и ExtendedAction, но это будет похоже на простое получение средств.

Коды возврата

Код возвратаОписание
0x84Попытка аутентификации через подпись, когда она отключена
0x85Проверка seqno не пройдена, сработала защита от повторного воспроизведения
0x86wallet_id не соответствует сохранённому
0x87Проверка подписи Ed25519 не пройдена
0x88Проверка valid-until не пройдена
0x89Для внешних сообщений обязательно использование бита +2 (игнорировать ошибки) в send_mode.
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() возвращает текущий идентификатор субкошелька.
  4. int get_public_key() возвращает текущий сохраненный публичный ключ.
  5. cell get_extensions() возвращает словарь расширений.

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

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

image

подсказка

Следовательно, возникают сервисы вроде Батареи Tonkeeper, которые предоставляют эту функциональность: они оплачивают комиссии за транзакции в TON от имени пользователя, но взимают комиссию в токенах.

Последовательность

  1. При отправке USDt пользователь подписывает одно сообщение, содержащее два исходящих перевода USDt:
    1. Перевод USDt на адрес получателя.
    2. Перевод небольшой суммы USDt в пользу сервиса.
  2. Это подписанное сообщение отправляется оффчейн по HTTPS на бекенд сервиса. Бекенд сервиса отправляет его в блокчейн TON, оплачивая сетевые комиссии в Toncoin.

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

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

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

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

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

Высоконагруженные кошельки (higload wallets)

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

Кошелёк с блокировкой (lockup wallet)

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

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

Например, вы можете создать кошелёк, который будет хранить 1 миллион монет с общим периодом вестинга в 10 лет. Можно установить период в один год после создания кошелька, когда средства будут заблокированы полностью. Для остального времени вы можете установить период разблокировки в один месяц, так что 1'000'000 TON / 120 месяцев = ~8333 TON будут разблокироваться каждый месяц.

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

Ограниченный кошелёк (restricted wallet)

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

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

Заключение

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

См. также

Was this article useful?