Кошельковые контракты
В TON даже кошелёк пользователя — это смарт-контракт, а не просто пара ключей. Это фундаментальное отличие от Ethereum, где аккаунты бывают «внешними» (EOA) и контрактами. Понимание устройства кошельковых контрактов (v3, v4, v5) необходимо для реализации авторизации, мультиподписи и интеграции с TON Connect.
В Ethereum кошелёк — это просто пара ключей (EOA): закрытый ключ подписывает транзакции, а адрес вычисляется из открытого ключа. В TON всё иначе: каждый кошелёк — это полноценный смарт-контракт. Когда вы отправляете TON через Tonkeeper или другое приложение, на самом деле ваш кошелёк-контракт получает подписанное внешнее сообщение, проверяет подпись и отправляет внутренние сообщения от вашего имени.
Понимание внутренней структуры кошельков помогает отлаживать транзакции, строить более надёжные dApp-интеграции и понимать поток газа в TON.
Основы кошельковых контрактов
Кошелёк как смарт-контракт
Каждый кошелёк TON хранит в своём persistent storage:
- Открытый ключ (public key) — для проверки подписей входящих внешних сообщений
- seqno (sequence number) — порядковый номер следующего сообщения (защита от повторного воспроизведения)
- subwallet_id — идентификатор подкошелька (позволяет создавать несколько кошельков из одного ключа)
Баланс TON не хранится в данных контракта. Баланс — это свойство аккаунта на уровне протокола, а не переменная в коде контракта. Это отличает TON от токенов ERC-20 в Ethereum, где балансы хранятся в mapping внутри контракта.
Поток обработки сообщений
Подробнее о том, как формируются и отправляются транзакции через кошелёк, см. в M07-L03 (Sending Transactions) и M03-L08 (Деплой в тестнет).
Эволюция версий
Сравнительная таблица
| Возможность | v3R2 | v4R2 | v5 (W5) |
|---|---|---|---|
| Защита seqno | Да | Да | Да |
| Subwallet ID | Да | Да | Да |
| Макс. сообщений | 4 | 4 | 255 |
| Плагины/расширения | Нет | Плагины | Расширения |
| Gasless-транзакции | Нет | Нет | Да |
| Снижение комиссий | — | — | ~25% |
| Статус | Legacy | Legacy | Текущий стандарт |
Wallet v3R2
Базовый кошелёк, ставший стандартом на ранних этапах TON:
- seqno — каждое сообщение увеличивает порядковый номер на 1, что предотвращает повторное воспроизведение (replay) одного и того же подписанного сообщения
- subwallet_id — позволяет создать несколько независимых кошельков из одной пары ключей (каждый с уникальным subwallet_id)
- Максимум 4 исходящих сообщения за одну транзакцию
- Простая и надёжная конструкция, но ограниченная функциональность
Wallet v4R2
Расширение v3R2 с системой плагинов:
- Всё то же, что в v3R2 (seqno, subwallet_id, 4 сообщения)
- Система плагинов — доверенные контракты, которые могут запрашивать TON с кошелька без подписи пользователя
- Основной сценарий плагинов — подписки: сервис-контракт периодически списывает оплату с кошелька
- Пользователь устанавливает и удаляет плагины явно
Wallet v5 (W5)
Текущий стандарт, разработанный командой Tonkeeper:
- До 255 исходящих сообщений за одну транзакцию (вместо 4)
- Gasless-транзакции — возможность оплаты газа через релееров (пользователь не тратит TON)
- Расширения — более гибкая замена плагинов v4, с поддержкой произвольных действий
- ~25% снижение комиссий за счёт оптимизации кода
- Поддержка аутентификации через внутренние сообщения (не только внешние) — основа для gasless
Gasless-транзакции (v5)
Одно из ключевых нововведений v5 — возможность проведения транзакций без TON на балансе кошелька:
Как это работает:
- Пользователь подписывает сообщение и передаёт его релееру (off-chain серверу)
- Релеер оборачивает подписанное сообщение во внутреннее сообщение и отправляет его кошельку v5
- Кошелёк v5 проверяет подпись (аутентификация через внутреннее сообщение — уникальная возможность v5)
- Кошелёк выполняет запрошенные действия
- Релеер получает компенсацию — например, в USDT вместо TON
Gasless-транзакции не означают “бесплатные”. Газ всё равно оплачивается — просто не пользователем напрямую. Релеер берёт компенсацию в другой форме (стейблкоинами, через модель подписки сервиса и т.д.).
Чтение исходного кода кошелька
FunC-код показан исключительно для навыка чтения существующих контрактов. Для разработки новых контрактов используйте Tact или Tolk.
Рассмотрим ключевые фрагменты recv_external кошелька v4R2 на FunC:
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512); ;; 1. Читаем подпись
var cs = in_msg;
var (subwallet_id, valid_until, msg_seqno) =
(cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
;; 2. Проверяем срок действия
throw_if(35, valid_until <= now());
;; 3. Загружаем данные контракта
var (stored_seqno, stored_subwallet, public_key, plugins) =
get_data().begin_parse()...;
;; 4. Проверяем seqno (защита от replay)
throw_unless(33, msg_seqno == stored_seqno);
;; 5. Проверяем subwallet_id
throw_unless(34, subwallet_id == stored_subwallet);
;; 6. Проверяем подпись Ed25519
throw_unless(35,
check_signature(slice_hash(in_msg), signature, public_key));
;; 7. Увеличиваем seqno и сохраняем
set_data(...stored_seqno + 1...);
;; 8. Отправляем внутренние сообщения
...send_raw_message(msg, mode)...
}
Ключевые моменты:
- Подпись проверяется первой — если подпись неверна, транзакция отклоняется с минимальным расходом газа
- seqno увеличивается атомарно — повторная отправка того же сообщения не пройдёт
- valid_until предотвращает выполнение устаревших сообщений
Развёртывание кошелька
Как кошелёк появляется в блокчейне:
- Вычисление адреса — адрес = hash(initial_code + initial_data). Адрес кошелька можно вычислить до деплоя
- Пополнение — на вычисленный адрес отправляются TON (кошелёк ещё не существует как контракт, но адрес уже принимает средства)
- Первое внешнее сообщение — содержит
state_init(код + начальные данные) + подписанное сообщение. Блокчейн создаёт контракт и выполняет первую транзакцию
state_init = {
code: <байткод кошелька v5>,
data: <seqno=0, subwallet_id=N, public_key=...>
}
address = hash(state_init) → EQ...
Этот механизм объяснён подробнее в M01-L05 (Addresses and Accounts) и M03-L08 (Деплой в тестнет). Ключевой момент: адрес детерминирован — разные версии кошелька с одним и тем же ключом дают разные адреса.
Практические последствия для разработчиков
Понимание кошельковых контрактов важно при разработке dApp:
- TON Connect интеграция — нужно учитывать, что разные пользователи могут использовать разные версии кошельков с разными возможностями (количество сообщений, gasless)
- Батчинг транзакций — v5 позволяет отправить до 255 сообщений одной транзакцией, что значительно упрощает сложные операции (multi-swap, batch transfer)
- Gasless UX — v5 позволяет строить интерфейсы, где пользователь вообще не взаимодействует с TON напрямую, оплачивая всё в стейблкоинах
- Replay protection — при построении сервисов, принимающих транзакции, учитывайте seqno-механизм: одно и то же подписанное сообщение не может быть выполнено дважды
Ключевые выводы
- Каждый кошелёк TON — это смарт-контракт, а не просто пара ключей: он хранит public key, seqno и subwallet_id
- Баланс TON — свойство протокола, а не переменная в контракте
- v5 (W5) — текущий стандарт: 255 сообщений, gasless-транзакции, расширения, ~25% экономия на газе
- Gasless работает через релееров: пользователь подписывает, релеер оплачивает газ и получает компенсацию в другой форме
- Адрес кошелька = hash(code + data): разные версии кошелька с одним ключом дают разные адреса
Частые ошибки
- Предполагают, что все кошельки работают одинаково, хотя версии v3, v4 и v5 имеют разную функциональность (plugins, extensions) и разные форматы сообщений.
- Не учитывают seqno при отправке транзакций: каждое внешнее сообщение кошелька должно содержать корректный sequence number, иначе будет отклонено.
- Забывают, что неинициализированный кошелёк (без деплоя) не может отправлять сообщения — сначала нужно пополнить и развернуть контракт.
- Хардкодят wallet_id, хотя разные сети (mainnet/testnet) используют разные subwallet_id для одного seed.
Проверка знанийПочему кошелёк v5 может проводить gasless-транзакции, а v3R2 и v4R2 — нет?
Проверьте понимание
Закончили урок?
Отметьте его как пройденный, чтобы отслеживать свой прогресс
Войдите чтобы оценить урок