Работа со смарт-контрактами кошелька
👋 Введение
Прежде чем приступать к разработке смарт-контрактов, важно изучить как работают кошельки и транзакции на TON. Это поможет разработчикам понять принцип взаимодействия между кошельками, сообщениями и смарт-контрактами для выполнения конкретных задач разработки.
Перед чтением руководства рекомендуется ознакомиться со статьей Типы контрактов кошелька.
Мы научимся создавать операции без использования предварительно настроенных функций, это полезно для лучшего понимания процесса разработки. Дополнительные ссылки на материалы для изучения находятся в конце каждого раздела.
💡 Перед началом работы
Изучение данного руководства потребует базовых знаний JavaScript и TypeScript или Golang. На балансе кошелька должно быть как минимум 3 TON (это может быть биржевой счет, некастодиальный кошелек или бот Кошелек от Telegram). Также необходимо иметь представление об адресах в TON, ячейке, блокчейне блокчейнов.
Работа с TON Testnet часто приводит к ошибкам при развертывании, сложностям с отслеживанием транзакций и нестабильной работе сети. Большую часть разработки лучше реализовать в TON Mainnet, чтобы избежать проблем, которые могут возникнуть при попытках уменьшить количество транзакций, и понизить сборы, соответственно.
💿 Исходный код
Все примеры кода, используемые в руководстве, можно найти в репозитории GitHub.
✍️ Что нужно для начала работы
- Установленный NodeJS
- Специальные библиотеки TON: @ton/ton 13.5.1+, @ton/core 0.49.2+ и @ton/crypto 3.2.0+
ОПЦИ ОНАЛЬНО: Если вы предпочитаете использовать Go, а не JS, то для разработки на TON необходимо установить GoLand IDE и библиотеку tonutils-go. Эта библиотека будет использоваться в данном руководстве для версии Go.
- JavaScript
- Golang
npm i --save @ton/ton @ton/core @ton/crypto
go get github.com/xssnick/tonutils-go
go get github.com/xssnick/tonutils-go/adnl
go get github.com/xssnick/tonutils-go/address
⚙ Настройте свое окружение
Для создания проекта TypeScript выполните следующие шаги:
- Создайте пустую папку (мы назовем ее WalletsTutorial).
- Откройте папку проекта с помощью CLI.
- Используйте следующие команды для настройки проекта:
npm init -y
npm install typescript @types/node ts-node nodemon --save-dev
npx tsc --init --rootDir src --outDir build \ --esModuleInterop --target es2020 --resolveJsonModule --lib es6 \ --module commonjs --allowJs true --noImplicitAny false --allowSyntheticDefaultImports true --strict false
Процесс ts-node запускает выполнение кода TypeScript без предварительной компиляции, а nodemon используется для автоматического перезапуска приложения node при обнаружении изменений файлов в директории.
"files": [
"\\",
"\\"
]
- Затем создайте конфигурацию
nodemon.jsonв корне проекта со следующим содержанием:
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "npx ts-node ./src/index.ts"
}
- Добавьте этот скрипт в
package.jsonвместо "test", который добавляется при создании проекта:
"start:dev": "npx nodemon"
- Создайте папку
srcв корне проекта и файлindex.tsв этой папке. - Далее добавьте следующий код:
async function main() {
console.log("Hello, TON!");
}
main().finally(() => console.log("Exiting..."));
- Запустите код в терминале:
npm run start:dev
- В итоге в консоли появится следующий вывод:

TON Community создали отличный инструмент для автоматизации всех процессов разработки (развертывание, написание контрактов, тестирование) под названием Blueprint. Однако нам не понадобится такой мощный инструмент, поэтому следует держаться приведенных выше инструкций.
ОПЦИОНАЛЬНО: При использовании Golang выполните следующие шаги:
- Установите GoLand IDE.
- Создайте папку проекта и файл
go.modсо следующим содержанием (для выполнения этого процесса может потребоваться изменить версию Go, если текущая используемая версия устарела):
module main
go 1.20
- Введите следующую команду в терминал:
go get github.com/xssnick/tonutils-go
- Создайте файл
main.goв корне проекта со следующим содержанием:
package main
import (
"log"
)
func main() {
log.Println("Hello, TON!")
}
- Измените наименование модуля в файле
go.modнаmain. - Запустите код выше до появления вывода в терминале.
Также можно использовать другую IDE, поскольку GoLand не бесплатна, но она предпочтительнее.
В каждом последующем разделе руководства будут указаны только те импорты, которые необходимы для конкретного раздела кода, новые импорты нужно будет добавлять и объединять со старыми.
🚀 Давайте начнем!
В этом разделе мы узнаем, какие кошельки (V3 и V4) чаще всего используются на блокчейне TON, и как работают их смарт-контракты. Это позволит разработчикам лучше понять различные типы сообщений на платформе TON, упростить их создание и отправку в блокчейн, научиться разворачивать кошельки и, в конечном итоге, работать с highload-кошельками.
Наша основная задача – научиться создавать сообщения, используя различные объекты и функции: @ton/ton, @ton/core, @ton/crypto (ExternalMessage, InternalMessage, Signing и т.д.), чтобы понять, как выглядят сообщения в более широких масштабах. Для этого мы будем использовать две основные версии кошелька (V3 и V4), поскольку биржи, некастодиальные кошельки и большинство пользователей используют именно эти версии.
There may be occasions in this tutorial when there is no explanation for particular details. In these cases, more details will be provided in later stages of this tutorial.
ВАЖНО: В данном руководстве используется код кошелька V3. Следует отметить, что версия 3 имеет две ревизии: R1 и R2. В настоящее время используется только вторая ревизия, поэтому, когда мы по тексту ссылаемся на V3, это означает V3R2.
💎 Кошельки TON Blockchain
Все кошельки, работающие на блокчейне TON, являются смарт-контрактами, и все, что работает на TON функционирует как смарт-контракт. Как и в большинстве блокчейнов TON позволяет разворачивать смарт-контракты и модифицировать их для различных целей, предоставляя возможность полной кастомизация кошелька. В TON смарт-контракты кошелька облегчают взаимодействие между платформой и другими типами смарт-контрактов. Однако важно понимать, как происходит данное взаимодействие.
Взаимодействие с кошельком
В блокчейне TON существует два типа сообщений: internal (внутренние) и external (внешние). Внешние сообщения позволяют отправлять сообщения в блокчейн из внешнего мира, тем самым обеспечивая связь со смарт-контрактами, которые принимают такие сообщения. Функция, отвечающая за выполнение этого процесса, выглядит следующим образом:
() recv_external(slice in_msg) impure {
;; some code
}
Перед более подробным изучением кошельков давайте рассмотрим, как они принимают внешние сообщения. На TON каждый кошелек хранит public key, seqno и subwallet_id владельца. При получении внешнего сообщения кошелек использует метод get_data() для извлечения данных из хранилища. Затем проводится несколько процедур верификации и определяется, принимать сообщение или нет. Это происходит следующим образом:
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512); ;; get signature from the message body
var cs = in_msg;
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32)); ;; get rest values from the message body
throw_if(35, valid_until <= now()); ;; check the relevance of the message
var ds = get_data().begin_parse(); ;; get data from storage and convert it into a slice to be able to read values
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256)); ;; read values from storage
ds.end_parse(); ;; make sure we do not have anything in ds variable
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, subwallet_id == stored_subwallet);
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
accept_message();
💡 Полезные ссылки:
Теперь давайте рассмотрим подробнее.
Защита от повторения – Seqno
Защита от повторения сообщений в смарт-контракте кошелька основана на seqno (Sequence Number) – порядковом номере, который отслеживает порядок отправляемых сообщений. Предотвращение повторения сообщений очень важно, так как дубликаты могут поставить под угрозу целостность системы. Если изучить код смарт-контракта кошелька, то seqno обычно обрабатывается следующим образом:
throw_unless(33, msg_seqno == stored_seqno);
Код выше сравнивает seqno, пришедшее во входящем сообщении с seqno, которое хранится в смарт-контракте. Если значения не совпадают, то контракт возвращает ошибку с кодом завершения 33. Таким образом, предоставление отправителем недействительного seqno указывает на ошибку в последовательности сообщений, смарт-контракт предотвращает дальнейшую обработку и гарантирует защиту от таких случаев.
Также важно учитывать, что внешние сообщения могут быть отправлены кем угодно. Это означает, что, если вы отправите кому-то 1 TON, кто-то другой сможет повторить это сообщение. Однако, когда seqno увеличивается, предыдущее внешнее сообщение становится нед ействительным, а значит никто не сможет его повторить, что предотвращает возможность кражи ваших средств.
Подпись
Как уже упоминалось ранее, смарт-контракты кошелька принимают внешние сообщения. Однако, поскольку эти сообщения приходят из внешнего мира, таким данным нельзя полностью доверять. Поэтому в каждом кошельке хранится публичный ключ владельца. Когда кошелек получает внешнее сообщение, подписанное приватным ключом владельца, смарт-контракт использует публичный ключ для проверки подписи сообщения. Это гарантирует, что сообщение пришло именно от владельца контракта.
Чтобы выполнить эту проверку кошелек сначала извлекает подпись из входящего сообщения, затем загружает публичный ключ из хранилища, и проверяет подпись с помощью следующих процедур:
var signature = in_msg~load_bits(512);
var ds = get_data().begin_parse();
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
Если все процедуры верификации завершены корректно, смарт-контракт принимает сообщение и обрабатывает сообщение:
accept_message();
Поскольку внешние сообщения не содержат Toncoin, необходимых для оплаты комиссии за транзакцию, функция accept_message() применяет параметр gas_credit (в настоящее время его значение составляет 10 000 единиц газа). Это позволяет контракту производить необходимые расчеты бесплатно, если газ не превышает значение gas_credit. После вызова функции accept_message() смарт-контракт вычитает все затраты на газ (в TON) из своего баланса. Подробнее об этом процессе можно прочитать здесь.
Срок действия транзакции
Еще одним шагом, используемым для проверки действительности внешних сообщений, является поле valid_until. Как видно из наименования переменной, это время в UNIX до которого сообщение будет действительным. Если процесс проверки завершился неудачей, контракт завершает обработку транзакции и возвращает код завершения 35:
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
throw_if(35, valid_until <= now());
Этот алгоритм защищает от потенциальных ошибок, например, когда сообщение уже недействительно, но по-прежнему отправляется в блокчейн по неизвестной причине.
Различия кошелька V3 и V4
Ключевое различие между V3 и V4 кошелька заключается в поддержке кошельком V4 плагинов, пользователи могут устанавливать или удалять данные плагины, которые представляют собой специализированные смарт-контракты, способные запрашивать в назначенное время определенное количество TON из смарт-контракта кошелька.
Смарт-контракты кошелька, в свою очередь, на запросы плагинов автоматически отправляют в ответ нужное количество TON без необходимости участия владельца. Эта функция отражает модель подписки, которая является основным назначением плагинов. Мы не будем углубляться в детали далее, поскольку это выходит за рамки данного руководства.
Как кошельки облегчают взаимодействие со смарт-контрактами
Как мы уже говорили, смарт-контракт кошелька принимает внешние сообщения, проверяет их и обрабатывает, если все проверки пройдены. Затем контракт запускает цикл извлечения сообщений из тела внешнего сообщения, после чего создает внутренние сообщения и отправляет их в блокчейн:
cs~touch();
while (cs.slice_refs()) {
var mode = cs~load_uint(8); ;; load message mode
send_raw_message(cs~load_ref(), mode); ;; get each new internal message as a cell with the help of load_ref() and send it
}
touch()На TON все смарт-контракты выполняются в виртуальной машине TON (TVM), основанной на стековой процессорной архитектуре. ~ touch() помещает переменную cs на вершину стека, чтобы оптимизировать выполнение кода для меньшего расхода газа.
Поскольку в одной ячейке может храниться максимум 4 ссылки, то на одно внешнее сообщение можно отправить максимум 4 внутренних сообщения.
💡 Полезные ссылки: