Работа со смарт-контрактами кошелька
👋 Введение
Прежде чем приступать к разработке смарт-контрактов, важно изучить как работают кошельки и транзакции на 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 внутренних сообщения.
💡 Полезные ссылки:
📬 Внешние и внутренние сообщения
В этом разделе мы узнаем чуть больше о внутренних internal
и внешних external
сообщениях. Мы создадим и отправим сообщения в сеть, при этом постараемся свести к минимуму зависимость от заранее созданных функций.
Для упрощения задачи воспользуемся готовым кошельком. Для этого:
- Установите приложение кошелька (например, автор использует Tonkeeper).
- В настройках приложения кошелька перейдите на версию V3R2.
- Внесите 1 TON на кошелек.
- Отправьте сообщение на другой адрес (можно отправить его себе на этот же кошелек).
В результате приложение Tonkeeper развернет смарт-контракт кошелька, к оторый мы сможем использовать для следующих шагов.
На момент написания руководства большинство приложений-кошельков на TON по умолчанию используют кошелек V4. В этом разделе плагины не требуются, а значит будет достаточно функциональности кошелька V3. Tonkeeper позволяет пользователям выбрать нужную им версию кошелька, поэтому рекомендуется развернуть кошелек V3.
TL-B
Как упоминалось ранее, все в блокчейне TON – это смарт-контракт, состоящий из ячеек. Чтобы правильно сериализовать и десериализовать данные, нам нужны стандарты. Для этой цели был разработан TL-B
– универсальный инструмент для описания различных типов данных, структур и последовательностей внутри ячеек.
В этом разделе мы будем работать со схемой данных block.tlb. Этот файл будет незаменим при дальнейшей разработке, поскольку в нем описывается, как собираются различные типы ячеек. В нашем конкретном случае там представлена подробная информация о структуре и поведении внутренних и внешних сообщений.
В данном руководстве представлена общая информация. Для получения более подробной информации изучите документацию по TL-B.
CommonMsgInfo
Изначально каждое сообщение должно хранить CommonMsgInfo
(TL-B) или CommonMsgInfoRelaxed
(TL-B). Эти данные позволяют определить технические детали, относящиеся к типу и времени сообщения, адресу получателя, техническим флагам и сборам.
Читая файл block.tlb
, можно заметить три типа CommonMsgInfo: int_msg_info$0
, ext_in_msg_info$10
, ext_out_msg_info$11
. Если не углубляться в детали, то конструктор ext_out_msg_info
– это внешнее сообщение, которое может отправляться смарт-контрактом в качестве внешнего лога. Как пример подобного формата изучите смарт-контракт Избирателя.
В схеме TL-B указано, что при использовании типа ext_in_msg_info доступен только CommonMsgInfo. Это происходит потому, что такие поля сообщения, как src
, created_lt
, created_at
и другие, перезаписываются валидаторами во время обработки транзакций. В данном случае поле src
наиболее важно, поскольку при отправке сообщения адрес отправителя неизвестен, и валидаторы заполняют данное поле во время верификации. Это гарантирует, что адрес src
достоверен и не может быть изменен.
Структура CommonMsgInfo
поддерживает спецификацию MsgAddress
, но так как адрес отправителя обычно неизвестен, то записывается addr_none$00
(00
– два нулевых бита), и в этом случае используется структура CommonMsgInfoRelaxed
, которая поддерживает addr_none
. Для ext_in_msg_info
(входящие внешние сообщения) используется структура CommonMsgInfo
, поскольку эти типы сообщений используют данные не отправителя, а MsgAddressExt, что означает отсутствие необходимости перезаписывать данные.
Числа после символа $
– это биты, которые необходимо хранить в начале определенной структуры, для дальнейшей идентификации этих структур при чтении (десериализации).
Создание внутреннего сообщения
Внутренние сообщения используются для передачи сообщений между смарт-контрактами. При анализе контрактов, которые отправляют сообщения, включающие написание контрактов (таких как NFT и Jetons), часто используются следующие строки кода:
var msg = begin_cell()
.store_uint(0x18, 6) ;; or 0x10 for non-bounce
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
;; store something as a body
Рассмотрим 0x18
и 0x10
, которые представляют собой шестнадцатеричные числа, расположенные следующим образом (с учетом, что нужно выделить 6 бит): 011000
и 010000
. Это означает, что приведенный выше код можно переписать следующим образом:
var msg = begin_cell()
.store_uint(0, 1) ;; this bit indicates that we send an internal message according to int_msg_info$0
.store_uint(1, 1) ;; IHR Disabled
.store_uint(1, 1) ;; or .store_uint(0, 1) for 0x10 | bounce
.store_uint(0, 1) ;; bounced
.store_uint(0, 2) ;; src -> two zero bits for addr_none
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
;; store something as a body
Давайте рассмотрим подробно каждый параметр:
Параметр | Описание |
---|---|
IHR Disabled | В настоящее время отключена (это означает, что мы храним значение 1 ), поскольку Мгновенная Маршрутизация Гиперкуба (Instant Hypercube Routing) реализована не полностью. Опция будет необходима, когда в сети появится большое количество шардчейнов. Подробнее об IHR Disabled можно прочитать в tblkch.pdf (глава 2) |
Bounce | При отправке сообщений в процессе обработки смарт-контракта могут возникать различные ошибки. Чтобы избежать потери TON, необходимо установить Bounce значение 1 (true). В этом случае, если во время обработки транзакции возникнут ошибки в контракте, то сообщение и отправленное количество TON (за вычетом комиссии) будут возвращены отправителю. Подробнее о невозвращаемых сообщениях можно прочитать здесь |
Bounced | Возвращаемые сообщения (Bounced) – это сообщения, которые возвращаются отправителю из-за ошибки, возникшей при обработке транзакции с помощью смарт-контракта. Этот параметр сообщает о том, является ли полученное сообщение вернувшимся |
Src | Адрес отправителя. В этом случае записываются два нулевых бита, чтобы указать адрес addr_none |
Следующие две строки кода:
...
.store_slice(to_address)
.store_coins(amount)
...
- указываем получателя и количество TON для отправки.
Посмотрим на оставшиеся строки кода:
...
.store_uint(0, 1) ;; Extra currency
.store_uint(0, 4) ;; IHR fee
.store_uint(0, 4) ;; Forwarding fee
.store_uint(0, 64) ;; Logical time of creation
.store_uint(0, 32) ;; UNIX time of creation
.store_uint(0, 1) ;; State Init
.store_uint(0, 1) ;; Message body
;; store something as a body
Параметр | Описание |
---|---|
Extra currency | Дополнительная валюта. Это первичная реализация существующих жетонов, и в настоящее время она не используется |
IHR fee | Комиссия IHR. Как уже говорилось, IHR в настоящее время не используется, поэтому данная комиссия всегда равна нулю. Подробнее об этом можно прочитать в tblkch.pdf (3.1.8) |
Forwarding fee | Комиссия за пересылку сообщения. Подробнее об этом можно прочитать здесь |
Logical time of creation | Время, затраченное на создание корректной очереди сообщений |
UNIX time of creation | Время создания сообщения в UNIX |
State Init | Код и исходные данные для развертывания смарт-контракта. Если бит установлен в 0 , это означает, что у нас нет State Init. Но если он установлен в 1 , то необходимо записать еще один бит, который будет указывать, хранится ли State Init в той же ячейке (0 ) или записан как ссылка (1 ) |
Message body | Тело сообщения. Параметр определяет, как хранится тело сообщения. Иногда тело сообщения слишком велико, чтобы поместиться в само сообщение. В этом случае его следует хранить как ссылку, при этом бит устанавливается в 1 , чтобы показать, что тело сообщения используется в качестве ссылки. Если бит равен 0 , тело находится в той же ячейке, что и сообщение |
Значения, описанные выше (включая src
), за исключением битов State Init
и Message Body
, перезаписываются валидаторами.
Если значение числа умещается в меньшее количество бит, чем установлено, то недостающие нули добавляются к левой части значения. Например, 0x18 умещается в 5 бит -> 11000
. Однако, как было сказано выше, требуется 6 бит, тогда конечный результат принимается 011000
.
Приступим к формированию сообщения, которое будет отправлено с некоторым количеством Toncoin на другой кошелек V3. Для начала, допустим, пользователь хочет отправить 0.5 TON самому себе с текстом Hello, TON!. Чтобы узнать как отправить сообщение с комментарием обратитесь к этому разделу документации.
- JavaScript
- Golang
import { beginCell } from '@ton/core';
let internalMessageBody = beginCell()
.storeUint(0, 32) // write 32 zero bits to indicate that a text comment will follow
.storeStringTail("Hello, TON!") // write our text comment
.endCell();
import (
"github.com/xssnick/tonutils-go/tvm/cell"
)
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32). // write 32 zero bits to indicate that a text comment will follow
MustStoreStringSnake("Hello, TON!"). // write our text comment
EndCell()
Выше мы создали ячейку InternalMessageBody
, в которой хранится тело нашего сообщения. Обратите внимание, что при хранении текста, который не помещается в одну ячейку (1023 бита), необходимо разбить данные на несколько ячеек в соответствии со следующей документацией. Однако, в нашем случае высокоуровневые библиотеки создают ячейки в соответствии с требованиями, поэтому на данном этапе об этом не стоит беспокоиться.
Затем, в соответствии с ранее изученной информацией, создается InternalMessage
:
- JavaScript
- Golang
import { toNano, Address } from '@ton/ton';
const walletAddress = Address.parse('put your wallet address');
let internalMessage = beginCell()
.storeUint(0, 1) // indicate that it is an internal message -> int_msg_info$0
.storeBit(1) // IHR Disabled
.storeBit(1) // bounce
.storeBit(0) // bounced
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress)
.storeCoins(toNano("0.2")) // amount
.storeBit(0) // Extra currency
.storeCoins(0) // IHR Fee
.storeCoins(0) // Forwarding Fee
.storeUint(0, 64) // Logical time of creation
.storeUint(0, 32) // UNIX time of creation
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(internalMessageBody) // Store Message Body as a reference
.endCell();
import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
)
walletAddress := address.MustParseAddr("put your address")
internalMessage := cell.BeginCell().
MustStoreUInt(0, 1). // indicate that it is an internal message -> int_msg_info$0
MustStoreBoolBit(true). // IHR Disabled
MustStoreBoolBit(true). // bounce
MustStoreBoolBit(false). // bounced
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress).
MustStoreCoins(tlb.MustFromTON("0.2").NanoTON().Uint64()). // amount
MustStoreBoolBit(false). // Extra currency
MustStoreCoins(0). // IHR Fee
MustStoreCoins(0). // Forwarding Fee
MustStoreUInt(0, 64). // Logical time of creation
MustStoreUInt(0, 32). // UNIX time of creation
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(internalMessageBody). // Store Message Body as a reference
EndCell()
Создание сообщения
Для начала необходимо извлечь seqno
(порядковый номер) смарт-контракта нашего кошелька. Для этого создается Client
, который будет использоваться для отправки запроса на выполнение GET-метода seqno
. Также необходимо добавить seed-фразу (которую вы сохранили при создании кошелька здесь), чтобы подписать наше сообщение, выполнив следующие действия:
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
import { mnemonicToWalletKey } from '@ton/crypto';
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC", // you can replace it on https://testnet.toncenter.com/api/v2/jsonRPC for testnet
apiKey: "put your api key" // you can get an api key from @tonapibot bot in Telegram
});
const mnemonic = 'put your mnemonic'; // word1 word2 word3
let getMethodResult = await client.runMethod(walletAddress, "seqno"); // run "seqno" GET method from your wallet contract
let seqno = getMethodResult.stack.readNumber(); // get seqno from response
const mnemonicArray = mnemonic.split(' '); // get array from string
const keyPair = await mnemonicToWalletKey(mnemonicArray); // get Secret and Public keys from mnemonic
import (
"context"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"golang.org/x/crypto/pbkdf2"
"log"
"strings"
)
mnemonic := strings.Split("put your mnemonic", " ") // get our mnemonic as array
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection) // create client
block, err := client.CurrentMasterchainInfo(context.Background()) // get current block, we will need it in requests to LiteServer
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
getMethodResult, err := client.RunGetMethod(context.Background(), block, walletAddress, "seqno") // run "seqno" GET method from your wallet contract
if err != nil {
log.Fatalln("RunGetMethod err:", err.Error())
return
}
seqno := getMethodResult.MustInt(0) // get seqno from response
// The next three lines will extract the private key using the mnemonic phrase. We will not go into cryptographic details. With the tonutils-go library, this is all implemented, but we’re doing it again to get a full understanding.
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
privateKey := ed25519.NewKeyFromSeed(k)
Итак, необходимо отправить seqno
, keys
и internal message
. Теперь нам нужно создать сообщение для нашего кошелька и сохранить данные в этом сообщении в той последовательности, которая использовалась в начале руководства. Это делается следующим образом:
- JavaScript
- Golang
import { sign } from '@ton/crypto';
let toSign = beginCell()
.storeUint(698983191, 32) // subwallet_id | We consider this further
.storeUint(Math.floor(Date.now() / 1e3) + 60, 32) // Message expiration time, +60 = 1 minute
.storeUint(seqno, 32) // store seqno
.storeUint(3, 8) // store mode of our internal message
.storeRef(internalMessage); // store our internalMessage as a reference
let signature = sign(toSign.endCell().hash(), keyPair.secretKey); // get the hash of our message to wallet smart contract and sign it to get signature
let body = beginCell()
.storeBuffer(signature) // store signature
.storeBuilder(toSign) // store our message
.endCell();
import (
"time"
)
toSign := cell.BeginCell().
MustStoreUInt(698983191, 32). // subwallet_id | We consider this further
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32). // Message expiration time, +60 = 1 minute
MustStoreUInt(seqno.Uint64(), 32). // store seqno
MustStoreUInt(uint64(3), 8). // store mode of our internal message
MustStoreRef(internalMessage) // store our internalMessage as a reference
signature := ed25519.Sign(privateKey, toSign.EndCell().Hash()) // get the hash of our message to wallet smart contract and sign it to get signature
body := cell.BeginCell().
MustStoreSlice(signature, 512). // store signature
MustStoreBuilder(toSign). // store our message
EndCell()
Обратите внимание, что здесь в toSign
не было использовано .endCell()
. Дело в том, что в данном случае необходимо передать содержимое toSign непосредственно в тело сообщения. Если бы потребовалась запись ячейки, ее пришлось бы хранить в виде ссылки.
В дополнение к базовому процессу верификации, который мы изучили для смарт-контрактов кошелька V3, смарт-контракт кошелька V4 извлекает опкод, чтобы определить, требуется простой перевод или сообщение, связанное с плагином. Чтобы соответствовать этой версии, необходимо добавить функции storeUint(0, 8)
(JS/TS), MustStoreUInt(0, 8)
(Golang) после записи seqno и перед указанием режима транзакции.
Создание внешнего сообщения
Чтобы доставить любое внутреннее сообщение в блокчейн из внешнего мира, необходимо отправить его во внешнем сообщении. Как мы уже рассмотрели ранее, необходимо использовать только структуру ext_in_msg_info$10
, поскольку цель в том, чтобы отправить внешнее сообщение нашему контракту. Давайте создадим внешнее сообщение, которое будет отправлено в наш кошелек:
- JavaScript
- Golang
let externalMessage = beginCell()
.storeUint(0b10, 2) // 0b10 -> 10 in binary
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress) // Destination address
.storeCoins(0) // Import Fee
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(body) // Store Message Body as a reference
.endCell();
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // 0b10 -> 10 in binary
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress). // Destination address
MustStoreCoins(0). // Import Fee
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(body). // Store Message Body as a reference
EndCell()
Параметр | Описание |
---|---|
Src | Адрес отправителя. Поскольку входящее внешнее сообщение не может иметь отправителя, в нем всегда будет 2 нулевых бита (addr_none TL-B) |
Import Fee | Комиссия для оплаты импорта входящих внешних сообщений |
State Init | Начальное состояние. В отличие от внутреннего сообщения, State Init внутри внешнего сообщения необходимо для развертывания контракта из внешнего мира. State Init , используемый вместе с внутренним сообщением, позволяет одному контракту развернуть другой |
Message Body | Тело сообщения. Сообщение, которое должно быть отправлено в контракт на обработку |
0b10 (b – двоичный) обозначает двоичную запись. В этом процессе хранятся два бита: 1
и 0
. Таким образом, мы указываем, что это ext_in_msg_info$10
.
Теперь у нас есть готовое сообщение, которое можно отправить нашему контракту. Для этого его нужно сначала сериализовать в BOC
(Bag of Cells), а затем отправить с помощью следующего кода:
- JavaScript
- Golang
console.log(externalMessage.toBoc().toString("base64"))
client.sendFile(externalMessage.toBoc());
import (
"encoding/base64"
"github.com/xssnick/tonutils-go/tl"
)
log.Println(base64.StdEncoding.EncodeToString(externalMessage.ToBOCWithFlags(false)))
var resp tl.Serializable
err = client.Client().QueryLiteserver(context.Background(), ton.SendMessage{Body: externalMessage.ToBOCWithFlags(false)}, &resp)
if err != nil {
log.Fatalln(err.Error())
return
}
💡 Полезная ссылка:
В результате мы получили вывод нашего BOC в консоли и сообщение, отправленное на наш кошелек. Скопировав строку в кодировке base64, мо жно вручную отправить наше сообщение и получить хэш с помощью toncenter.
👛 Развертывание кошелька
Мы изучили основы создания сообщений, которые теперь пригодятся нам для развертывания кошелька. Ранее мы разворачивали кошелек с помощью приложения кошелька, но теперь сделаем это вручную.
В этом разделе мы рассмотрим, как создать кошелек (V3) с нуля. Вы узнаете, как скомпилировать код смарт-контракта кошелька, сгенерировать мнемоническую фразу, получить адрес кошелька и развернуть кошелек с помощью внешних сообщений и State Init (инициализация состояния).
Создание мнемоники
Первое, что необходимо для корректного создания кошелька – это получение приватного
и публичного
ключей. Чтобы выполнить эту задачу, необходимо сгенерировать мнемоническую seed-фразу, а затем извлечь приватный и публичный ключи с помощью криптографических библиотек.
Это делается следующим образом:
- JavaScript
- Golang
import { mnemonicToWalletKey, mnemonicNew } from '@ton/crypto';
// const mnemonicArray = 'put your mnemonic'.split(' ') // get our mnemonic as array
const mnemonicArray = await mnemonicNew(24); // 24 is the number of words in a seed phrase
const keyPair = await mnemonicToWalletKey(mnemonicArray); // extract private and public keys from mnemonic
console.log(mnemonicArray) // if we want, we can print our mnemonic
import (
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"log"
"github.com/xssnick/tonutils-go/ton/wallet"
"golang.org/x/crypto/pbkdf2"
"strings"
)
// mnemonic := strings.Split("put your mnemonic", " ") // get our mnemonic as array
mnemonic := wallet.NewSeed() // get new mnemonic
// The following three lines will extract the private key using the mnemonic phrase. We will not go into cryptographic details. It has all been implemented in the tonutils-go library, but it immediately returns the finished object of the wallet with the address and ready methods. So we’ll have to write the lines to get the key separately. Goland IDE will automatically import all required libraries (crypto, pbkdf2 and others).
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
// 32 is a key len
privateKey := ed25519.NewKeyFromSeed(k) // get private key
publicKey := privateKey.Public().(ed25519.PublicKey) // get public key from private key
log.Println(publicKey) // print publicKey so that at this stage the compiler does not complain that we do not use our variable
log.Println(mnemonic) // if we want, we can print our mnemonic
Приватный ключ необходим для подписания сообщений, а публичный ключ хранится в смарт-контракте кошелька.
Необходимо вывести сгенерированную мнемоническую seed-фразу в консоль, сохранить и использовать одну и ту же пару ключей при каждом запуске кода кошелька (как использовать подробно описано в предыдущем разделе).
Идентификаторы subwallet
Одно из наиболее заметных преимуществ того, что кошельки являются смарт-контрактами – это возможность создавать огромное количество кошельков, используя всего один приватный ключ. Это происходит потому, что адреса смарт-контрактов в блокчейне TON вычи сляются с помощью нескольких факторов, включая stateInit
. StateInit содержит code
и initial data
(первичные данные), которые хранятся в хранилище смарт-контрактов блокчейна.
Изменив всего один бит в stateInit
, можно сгенерировать другой адрес. Именно поэтому изначально был создан subwallet_id
. subwallet_id
хранится в хранилище контрактов и может быть использован для создания множества различных кошельков (с различными идентификаторами subwallet) с помощью одного приватного ключа. Эта функциональность может быть очень полезна при интеграции различных типов кошельков с централизованными сервисами, такими как биржи.
Значение subwallet_id
по умолчанию равно 698983191
, согласно строке кода ниже, взятой из исходного кода TON Blockchain:
res.wallet_id = td::as<td::uint32>(res.config.zero_state_id.root_hash.as_slice().data());
Информацию о блоке genesis (zero_state
) можно получить из файла конфигурации. Разбираться детально в этом не обязательно, но важно помнить, что по умолчанию значение subwallet_id
равно 698983191
.
Контракт каждого кошелька проверяет поле subwallet_id
для внешних сообщений, чтобы избежать случаев, когда запросы были отправлены на кошелек с другим идентификатором:
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
throw_unless(34, subwallet_id == stored_subwallet);
Нужно будет добавить вышеуказанное значение в initial data
(первичные данные) контракта, поэтому переменную нужно сохранить следующим образом:
- JavaScript
- Golang
const subWallet = 698983191;
var subWallet uint64 = 698983191
Компиляция кода кошелька
Теперь, когда у нас четко определены приватный и публичный ключи, а также subwallet_id, нужно скомпилировать код кошелька. Для этого воспользуемся кодом wallet V3 из официального репозитория.
Чтобы скомпилировать код кошелька необходимо использовать библиотеку @ton-community/func-js.
С помощью этой библиотеки можно скомпилировать код FunC и получить ячейку, содержащую этот код. Чтобы начать работу, необходимо установить библиотеку и сохранить ее в package.json
следующим образом:
npm i --save @ton-community/func-js
Для компиляции кода мы будем использовать только JavaScript, поскольку библиотеки для компиляции кода основаны на JavaScript. Однако после завершения компиляции, пока у нас есть base64-вывод нашей ячейки, можно использовать этот скомпилированный код в таких языках, как Go и других.
Для начала нам нужно создать два файла: wallet_v3.fc
и stdlib.fc
. Компилятор работает с библиотекой stdlib.fc
. Все необходимые и базовые функции, которые соответствуют инструкциям asm
, есть в этой библиотеке. Файл stdlib.fc
можно скачать здесь. В файл wallet_v3.fc
необходимо скопировать приведенный выше код.
Теперь у нас есть следующая структура для создаваемого проекта:
.
├── src/
│ ├── main.ts
│ ├── wallet_v3.fc
│ └── stdlib.fc
├── nodemon.json
├── package-lock.json
├── package.json
└── tsconfig.json
Ничего страшного, если IDE-плагин конфликтует с () set_seed(int) impure asm "SETRAND";
в файле stdlib.fc
.
Не забудьте добавить следующую строку в начало файла wallet_v3.fc
, чтобы указать, что ниже будут использоваться функции из stdlib
:
#include "stdlib.fc";
Теперь давайте напишем код для компиляции смарт-контракта и запустим его с помощью команды npm run start:dev
:
import { compileFunc } from '@ton-community/func-js';
import fs from 'fs'; // we use fs for reading content of files
import { Cell } from '@ton/core';
const result = await compileFunc({
targets: ['wallet_v3.fc'], // targets of your project
sources: {
"stdlib.fc": fs.readFileSync('./src/stdlib.fc', { encoding: 'utf-8' }),
"wallet_v3.fc": fs.readFileSync('./src/wallet_v3.fc', { encoding: 'utf-8' }),
}
});
if (result.status === 'error') {
console.error(result.message)
return;
}
const codeCell = Cell.fromBoc(Buffer.from(result.codeBoc, "base64"))[0]; // get buffer from base64 encoded BOC and get cell from this buffer
// now we have base64 encoded BOC with compiled code in result.codeBoc
console.log('Code BOC: ' + result.codeBoc);
console.log('\nHash: ' + codeCell.hash().toString('base64')); // get the hash of cell and convert in to base64 encoded string. We will need it further
В результате в терминале появится следующий вывод:
Code BOC: te6ccgEBCAEAhgABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQCW8oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOgwAaTIyx/LH8v/ye1UAATQMAIBSAYHABe7Oc7UTQ0z8x1wv/gAEbjJftRNDXCx+A==
Hash: idlku00WfSC36ujyK2JVT92sMBEpCNRUXOGO4sJVBPA=
После этого можно получить ту же самую ячейку (используя вывод в кодировке base64) с помощью кода кошелька, используя другие библиотеки и языки:
- Golang
import (
"encoding/base64"
"github.com/xssnick/tonutils-go/tvm/cell"
)
base64BOC := "te6ccgEBCAEAhgABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQCW8oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOgwAaTIyx/LH8v/ye1UAATQMAIBSAYHABe7Oc7UTQ0z8x1wv/gAEbjJftRNDXCx+A==" // save our base64 encoded output from compiler to variable
codeCellBytes, _ := base64.StdEncoding.DecodeString(base64BOC) // decode base64 in order to get byte array
codeCell, err := cell.FromBOC(codeCellBytes) // get cell with code from byte array
if err != nil { // check if there are any error
panic(err)
}
log.Println("Hash:", base64.StdEncoding.EncodeToString(codeCell.Hash())) // get the hash of our cell, encode it to base64 because it has []byte type and output to the terminal
В результате в терминале появится следующий вывод:
idlku00WfSC36ujyK2JVT92sMBEpCNRUXOGO4sJVBPA=
После завершения этих процессов подтверждается, что в нашей ячейке используется корректный код, поскольку хэши совпадают.
Создание State Init для развертывания
Прежде чем создавать сообщение, важно понять, что такое State Init. Для начала пройдемся по схеме TL-B:
Параметр | Описание |
---|---|
split_depth | Этот параметр предназначен для высоконагруженных смарт-контрактов, которые могут быть разделены и располагаться на нескольких шардчейнах. Более подробную информацию можно найти в tblkch.pdf (4.1.6). Хранится только бит 0 , поскольку он используется в рамках смарт-контракта кошелька |
special | Используется для TicTok. Эти смарт-контракты автоматически вызываются для каждого блока и не требуются для обычных смарт-контрактов. Информацию об этом можно найти в этом разделе или в tblkch.pdf (4.1.6). В данной спецификации хранится только бит 0 , поскольку такая функция нам не требуется |
code | Бит 1 означает наличие кода смарт-контракта в качестве ссылки |
data | Бит 1 означает наличие данных смарт-контракта в качестве ссылки |
library | Библиотека, которая работает с мастерчейном и может быть использована различными смарт-контрактами. Она не будет использоваться для кошелька, поэтому бит установлен в 0 . Информацию об этом можно найти в tblkch.pdf (1.8.4) |
Далее подготовим initial data
(первичные данные), которые будут представлены в хранилище нашего контракта сразу после развертывания:
- JavaScript
- Golang
import { beginCell } from '@ton/core';
const dataCell = beginCell()
.storeUint(0, 32) // Seqno
.storeUint(698983191, 32) // Subwallet ID
.storeBuffer(keyPair.publicKey) // Public Key
.endCell();
dataCell := cell.BeginCell().
MustStoreUInt(0, 32). // Seqno
MustStoreUInt(698983191, 32). // Subwallet ID
MustStoreSlice(publicKey, 256). // Public Key
EndCell()
На этом этапе присутствует как code
контракта, так и initial data
. С помощью этих данных мы можем создать наш адрес кошелька. Адрес кошелька зависит от State Init
, которое включает в себя код и первичные данные.
- JavaScript
- Golang
import { Address } from '@ton/core';
const stateInit = beginCell()
.storeBit(0) // No split_depth
.storeBit(0) // No special
.storeBit(1) // We have code
.storeRef(codeCell)
.storeBit(1) // We have data
.storeRef(dataCell)
.storeBit(0) // No library
.endCell();
const contractAddress = new Address(0, stateInit.hash()); // get the hash of stateInit to get the address of our smart contract in workchain with ID 0
console.log(`Contract address: ${contractAddress.toString()}`); // Output contract address to console
import (
"github.com/xssnick/tonutils-go/address"
)
stateInit := cell.BeginCell().
MustStoreBoolBit(false). // No split_depth
MustStoreBoolBit(false). // No special
MustStoreBoolBit(true). // We have code
MustStoreRef(codeCell).
MustStoreBoolBit(true). // We have data
MustStoreRef(dataCell).
MustStoreBoolBit(false). // No library
EndCell()
contractAddress := address.NewAddress(0, 0, stateInit.Hash()) // get the hash of stateInit to get the address of our smart contract in workchain with ID 0
log.Println("Contract address:", contractAddress.String()) // Output contract address to console
Используя State Init
, мы можем создать сообщение и отправить его в блокчейн.
To carry out this process, a minimum wallet balance of 0.1 TON is required (the balance can be less, but this amount is guaranteed to be sufficient). To accomplish this, we’ll need to run the code mentioned earlier in the tutorial, obtain the correct wallet address, and send 0.1 TON to this address. Alternatively, you can send this sum manually via your wallet app before sending the deployment message itself.
Развертывание с помощью внешних сообщений представлено здесь в основном в образовательных целях, на практике гораздо удобнее разворачивать смарт-контракты через кошельки, что будет описано позже.
Давайте начнем с создания сообщения, аналогичного тому, которое мы создали в предыдущем разделе:
- JavaScript
- Golang
import { sign } from '@ton/crypto';
import { toNano } from '@ton/core';
const internalMessageBody = beginCell()
.storeUint(0, 32)
.storeStringTail("Hello, TON!")
.endCell();
const internalMessage = beginCell()
.storeUint(0x10, 6) // no bounce
.storeAddress(Address.parse("put your first wallet address from were you sent 0.1 TON"))
.storeCoins(toNano("0.03"))
.storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) // We store 1 that means we have body as a reference
.storeRef(internalMessageBody)
.endCell();
// message for our wallet
const toSign = beginCell()
.storeUint(subWallet, 32)
.storeUint(Math.floor(Date.now() / 1e3) + 60, 32)
.storeUint(0, 32) // We put seqno = 0, because after deploying wallet will store 0 as seqno
.storeUint(3, 8)
.storeRef(internalMessage);
const signature = sign(toSign.endCell().hash(), keyPair.secretKey);
const body = beginCell()
.storeBuffer(signature)
.storeBuilder(toSign)
.endCell();
import (
"github.com/xssnick/tonutils-go/tlb"
"time"
)
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32).
MustStoreStringSnake("Hello, TON!").
EndCell()
internalMessage := cell.BeginCell().
MustStoreUInt(0x10, 6). // no bounce
MustStoreAddr(address.MustParseAddr("put your first wallet address from were you sent 0.1 TON")).
MustStoreBigCoins(tlb.MustFromTON("0.03").NanoTON()).
MustStoreUInt(1, 1 + 4 + 4 + 64 + 32 + 1 + 1). // We store 1 that means we have body as a reference
MustStoreRef(internalMessageBody).
EndCell()
// message for our wallet
toSign := cell.BeginCell().
MustStoreUInt(subWallet, 32).
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32).
MustStoreUInt(0, 32). // We put seqno = 0, because after deploying wallet will store 0 as seqno
MustStoreUInt(3, 8).
MustStoreRef(internalMessage)
signature := ed25519.Sign(privateKey, toSign.EndCell().Hash())
body := cell.BeginCell().
MustStoreSlice(signature, 512).
MustStoreBuilder(toSign).
EndCell()
После этого вы получите корректный State Init
и Message Body
.
Отправка внешнего сообщения
Основное отличие будет заключаться в наличии внешнего сообщения, поскольку State Init хранится для того, чтобы помочь выполнить корректное развертывание контракта. Поскольку у контракта еще нет собственного кода, он не может обрабатывать никакие внутренние сообщения. Поэтому мы отправим его код и первичные данные после того, как контракт будет успешно развернут, чтобы он смог обработать наше сообщение с комментарием "Hello, TON!":
- JavaScript
- Golang
const externalMessage = beginCell()
.storeUint(0b10, 2) // indicate that it is an incoming external message
.storeUint(0, 2) // src -> addr_none
.storeAddress(contractAddress)
.storeCoins(0) // Import fee
.storeBit(1) // We have State Init
.storeBit(1) // We store State Init as a reference
.storeRef(stateInit) // Store State Init as a reference
.storeBit(1) // We store Message Body as a reference
.storeRef(body) // Store Message Body as a reference
.endCell();
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // indicate that it is an incoming external message
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(contractAddress).
MustStoreCoins(0). // Import fee
MustStoreBoolBit(true). // We have State Init
MustStoreBoolBit(true). // We store State Init as a reference
MustStoreRef(stateInit). // Store State Init as a reference
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(body). // Store Message Body as a reference
EndCell()
Наконец-то, мы можем отправить сообщение в блокчейн, чтобы развернуть кошелек и использовать его.
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
apiKey: "put your api key" // you can get an api key from @tonapibot bot in Telegram
});
client.sendFile(externalMessage.toBoc());
import (
"context"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/tl"
"github.com/xssnick/tonutils-go/ton"
)
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection)
var resp tl.Serializable
err = client.Client().QueryLiteserver(context.Background(), ton.SendMessage{Body: externalMessage.ToBOCWithFlags(false)}, &resp)
if err != nil {
log.Fatalln(err.Error())
return
}
Обратите внимание, что мы отправили внутреннее сообщение, используя режим 3
. Если необходимо повторить развертывание одного и того же кошелька, смарт-контракт можно уничтожить. Для этого установите нужный режим, добавив 128 (забрать весь баланс смарт-контракта) + 32 (уничтожить смарт-контракт), что составит = 160
для извлечения оставшегося баланса TON и повторного развертывания кошелька.
Важно отметить, что для каждой новой т ранзакции seqno нужно будет увеличить на единицу.
Код контракта, который мы использовали – верифицирован, пример можно посмотреть здесь.
💸 Работа со смарт-контрактами кошелька
Теперь мы гораздо лучше знакомы со смарт-контрактами кошелька, с тем как они разрабатываются и используются. Мы научились разворачивать и уничтожать смарт-контракты, а также отправлять сообщения без использования предварительно настроенных библиотечных функци й. В следующем разделе мы сосредоточимся на создании и отправке более сложных сообщений, чтобы применить то, что мы узнали выше.
Отправка нескольких сообщений одновременно
Как вы, возможно, уже знаете, одна ячейка может хранить до 1023 бит данных и до 4 ссылок на другие ячейки. Ранее мы подробно описали, как внутренние сообщения извлекаются в "полном" цикле в виде ссылки, после чего отправляются в виде сообщения. Это означает, что внутри внешнего сообщения можно хранить до 4 внутренних сообщений. Это позволяет отправлять четыре сообщения одновременно.
Для этого необходимо создать 4 разных внутренних сообщения. Мы можем сделать это вручную или с помощью цикла – loop
. Сначала нужно определить 3 массива: массив количества TON, массив комментариев, массив сообщений. Для сообщений нужно подготовить еще один массив – internalMessages.
- JavaScript
- Golang
import { Cell } from '@ton/core';
const internalMessagesAmount = ["0.01", "0.02", "0.03", "0.04"];
const internalMessagesComment = [
"Hello, TON! #1",
"Hello, TON! #2",
"", // Let's leave the third message without comment
"Hello, TON! #4"
]
const destinationAddresses = [
"Put any address that belongs to you",
"Put any address that belongs to you",
"Put any address that belongs to you",
"Put any address that belongs to you"
] // All 4 addresses can be the same
let internalMessages:Cell[] = []; // array for our internal messages
import (
"github.com/xssnick/tonutils-go/tvm/cell"
)
internalMessagesAmount := [4]string{"0.01", "0.02", "0.03", "0.04"}
internalMessagesComment := [4]string{
"Hello, TON! #1",
"Hello, TON! #2",
"", // Let's leave the third message without comment
"Hello, TON! #4",
}
destinationAddresses := [4]string{
"Put any address that belongs to you",
"Put any address that belongs to you",
"Put any address that belongs to you",
"Put any address that belongs to you",
} // All 4 addresses can be the same
var internalMessages [len(internalMessagesAmount)]*cell.Cell // array for our internal messages
Режим отправки для всех сообщений установлен на mode 3
. Однако, если требуются разные режимы, можно создать массив для выполнения различных целей.
- JavaScript
- Golang
import { Address, beginCell, toNano } from '@ton/core';
for (let index = 0; index < internalMessagesAmount.length; index++) {
const amount = internalMessagesAmount[index];
let internalMessage = beginCell()
.storeUint(0x18, 6) // bounce
.storeAddress(Address.parse(destinationAddresses[index]))
.storeCoins(toNano(amount))
.storeUint(0, 1 + 4 + 4 + 64 + 32 + 1);
/*
At this stage, it is not clear if we will have a message body.
So put a bit only for stateInit, and if we have a comment, in means
we have a body message. In that case, set the bit to 1 and store the
body as a reference.
*/
if(internalMessagesComment[index] != "") {
internalMessage.storeBit(1) // we store Message Body as a reference
let internalMessageBody = beginCell()
.storeUint(0, 32)
.storeStringTail(internalMessagesComment[index])
.endCell();
internalMessage.storeRef(internalMessageBody);
}
else
/*
Since we do not have a message body, we indicate that
the message body is in this message, but do not write it,
which means it is absent. In that case, just set the bit to 0.
*/
internalMessage.storeBit(0);
internalMessages.push(internalMessage.endCell());
}
import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
)
for i := 0; i < len(internalMessagesAmount); i++ {
amount := internalMessagesAmount[i]
internalMessage := cell.BeginCell().
MustStoreUInt(0x18, 6). // bounce
MustStoreAddr(address.MustParseAddr(destinationAddresses[i])).
MustStoreBigCoins(tlb.MustFromTON(amount).NanoTON()).
MustStoreUInt(0, 1+4+4+64+32+1)
/*
At this stage, it is not clear if we will have a message body.
So put a bit only for stateInit, and if we have a comment, in means
we have a body message. In that case, set the bit to 1 and store the
body as a reference.
*/
if internalMessagesComment[i] != "" {
internalMessage.MustStoreBoolBit(true) // we store Message Body as a reference
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32).
MustStoreStringSnake(internalMessagesComment[i]).
EndCell()
internalMessage.MustStoreRef(internalMessageBody)
} else {
/*
Since we do not have a message body, we indicate that
the message body is in this message, but do not write it,
which means it is absent. In that case, just set the bit to 0.
*/
internalMessage.MustStoreBoolBit(false)
}
internalMessages[i] = internalMessage.EndCell()
}
Теперь давайте воспользуемся знаниями из предыдущего раздела, чтобы создать сообщение для кошелька, которое может отправлять 4 сообщения одновременно:
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
import { mnemonicToWalletKey } from '@ton/crypto';
const walletAddress = Address.parse('put your wallet address');
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
apiKey: "put your api key" // you can get an api key from @tonapibot bot in Telegram
});
const mnemonic = 'put your mnemonic'; // word1 word2 word3
let getMethodResult = await client.runMethod(walletAddress, "seqno"); // run "seqno" GET method from your wallet contract
let seqno = getMethodResult.stack.readNumber(); // get seqno from response
const mnemonicArray = mnemonic.split(' '); // get array from string
const keyPair = await mnemonicToWalletKey(mnemonicArray); // get Secret and Public keys from mnemonic
let toSign = beginCell()
.storeUint(698983191, 32) // subwallet_id
.storeUint(Math.floor(Date.now() / 1e3) + 60, 32) // Message expiration time, +60 = 1 minute
.storeUint(seqno, 32); // store seqno
// Do not forget that if we use Wallet V4, we need to add .storeUint(0, 8)
import (
"context"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"golang.org/x/crypto/pbkdf2"
"log"
"strings"
"time"
)
walletAddress := address.MustParseAddr("put your wallet address")
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection)
mnemonic := strings.Split("put your mnemonic", " ") // word1 word2 word3
// The following three lines will extract the private key using the mnemonic phrase.
// We will not go into cryptographic details. In the library tonutils-go, it is all implemented,
// but it immediately returns the finished object of the wallet with the address and ready-made methods.
// So we’ll have to write the lines to get the key separately. Goland IDE will automatically import
// all required libraries (crypto, pbkdf2 and others).
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
// 32 is a key len
privateKey := ed25519.NewKeyFromSeed(k) // get private key
block, err := client.CurrentMasterchainInfo(context.Background()) // get current block, we will need it in requests to LiteServer
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
getMethodResult, err := client.RunGetMethod(context.Background(), block, walletAddress, "seqno") // run "seqno" GET method from your wallet contract
if err != nil {
log.Fatalln("RunGetMethod err:", err.Error())
return
}
seqno := getMethodResult.MustInt(0) // get seqno from response
toSign := cell.BeginCell().
MustStoreUInt(698983191, 32). // subwallet_id | We consider this further
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32). // message expiration time, +60 = 1 minute
MustStoreUInt(seqno.Uint64(), 32) // store seqno
// Do not forget that if we use Wallet V4, we need to add MustStoreUInt(0, 8).
Далее добавим сообщения, которые мы создали ранее в цикле:
- JavaScript
- Golang
for (let index = 0; index < internalMessages.length; index++) {
const internalMessage = internalMessages[index];
toSign.storeUint(3, 8) // store mode of our internal message
toSign.storeRef(internalMessage) // store our internalMessage as a reference
}
for i := 0; i < len(internalMessages); i++ {
internalMessage := internalMessages[i]
toSign.MustStoreUInt(3, 8) // store mode of our internal message
toSign.MustStoreRef(internalMessage) // store our internalMessage as a reference
}
Теперь, когда все вышеперечисленные процессы завершены, давайте подпи шем наше сообщение, создадим внешнее сообщение (как описано в предыдущих разделах этого руководства) и отправим его в блокчейн:
- JavaScript
- Golang
import { sign } from '@ton/crypto';
let signature = sign(toSign.endCell().hash(), keyPair.secretKey); // get the hash of our message to wallet smart contract and sign it to get signature
let body = beginCell()
.storeBuffer(signature) // store signature
.storeBuilder(toSign) // store our message
.endCell();
let externalMessage = beginCell()
.storeUint(0b10, 2) // ext_in_msg_info$10
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress) // Destination address
.storeCoins(0) // Import Fee
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(body) // Store Message Body as a reference
.endCell();
client.sendFile(externalMessage.toBoc());
import (
"github.com/xssnick/tonutils-go/tl"
)
signature := ed25519.Sign(privateKey, toSign.EndCell().Hash()) // get the hash of our message to wallet smart contract and sign it to get signature
body := cell.BeginCell().
MustStoreSlice(signature, 512). // store signature
MustStoreBuilder(toSign). // store our message
EndCell()
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // ext_in_msg_info$10
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress). // Destination address
MustStoreCoins(0). // Import Fee
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(body). // Store Message Body as a reference
EndCell()
var resp tl.Serializable
err = client.Client().QueryLiteserver(context.Background(), ton.SendMessage{Body: externalMessage.ToBOCWithFlags(false)}, &resp)
if err != nil {
log.Fatalln(err.Error())
return
}
Если возникает ошибка, связанная с подключением к lite-серверу (Golang), код должен выполняться до тех пор, пока сооб щение не будет отправлено. Это происходит потому, что библиотека tonutils-go использует несколько различных lite-серверов через глобальную конфигурацию, которая была указана в коде. Однако не все lite-серверы могут принять наше соединение.
После завершения данного процесса можно воспользоваться обозревателем блокчейна TON, чтобы убедиться, что кошелек отправил четыре сообщения на указанные ранее адреса.
Передача NFT
Помимо обычных сообщений, пользователи часто отправляют друг другу NFT. К сожалению, не все библиотеки содержат методы, адаптированные для работы с этим типом смарт-контрактов. Поэтому необходимо создать код, который позволит нам выстроить сообщение для отправки NFT. Для начала давайте познакомимся с NFT-стандартом TON.
Давайте детально рассмотрим схему TL-B для NFT Transfers:
-
query_id
– Идент ификатор запроса не имеет никакого значения с точки зрения обработки сообщения. Контракт NFT не подтверждает его, а только считывает. Значение идентификатора может быть полезно, когда сервис хочет присвоить каждому своему сообщению определённый ID запроса для идентификации. Поэтому мы установим его в 0. -
response_destination
– После обработки сообщения о смене владельца появятся дополнительные TON. Они будут отправлены по этому адресу, если он указан, в противном случае останутся на балансе NFT. -
custom_payload
– Необходимо для выполнения специфических задач и не используется с обычными NFT. -
forward_amount
– Если значение forward_amount не равно нулю, указанное количество TON будет отправлено новому владельцу. Таким образом, новый владелец будет уведомлен о том, что он что-то получил. -
forward_payload
– Дополнительные данные, которые могут быть отправлены новому владельцу вместе сforward_amount
. Например, использованиеforward_payload
позволяет пользователям добавить комментарий во время передач и NFT, как было отмечено в руководстве ранее. Однако, несмотря на то, чтоforward_payload
написан в рамках стандарта TON для NFT, блокчейн-обозреватели не полностью поддерживают отображение различных деталей. Такая же проблема существует и при отображении жетонов.
Теперь давайте создадим само сообщение:
- JavaScript
- Golang
import { Address, beginCell, toNano } from '@ton/core';
const destinationAddress = Address.parse("put your wallet where you want to send NFT");
const walletAddress = Address.parse("put your wallet which is the owner of NFT")
const nftAddress = Address.parse("put your nft address");
// We can add a comment, but it will not be displayed in the explorers,
// as it is not supported by them at the time of writing the tutorial.
const forwardPayload = beginCell()
.storeUint(0, 32)
.storeStringTail("Hello, TON!")
.endCell();
const transferNftBody = beginCell()
.storeUint(0x5fcc3d14, 32) // Opcode for NFT transfer
.storeUint(0, 64) // query_id
.storeAddress(destinationAddress) // new_owner
.storeAddress(walletAddress) // response_destination for excesses
.storeBit(0) // we do not have custom_payload
.storeCoins(toNano("0.01")) // forward_amount
.storeBit(1) // we store forward_payload as a reference
.storeRef(forwardPayload) // store forward_payload as a .reference
.endCell();
const internalMessage = beginCell().
storeUint(0x18, 6). // bounce
storeAddress(nftAddress).
storeCoins(toNano("0.05")).
storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1). // We store 1 that means we have body as a reference
storeRef(transferNftBody).
endCell();
import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/tvm/cell"
)
destinationAddress := address.MustParseAddr("put your wallet where you want to send NFT")
walletAddress := address.MustParseAddr("put your wallet which is the owner of NFT")
nftAddress := address.MustParseAddr("put your nft address")
// We can add a comment, but it will not be displayed in the explorers,
// as it is not supported by them at the time of writing the tutorial.
forwardPayload := cell.BeginCell().
MustStoreUInt(0, 32).
MustStoreStringSnake("Hello, TON!").
EndCell()
transferNftBody := cell.BeginCell().
MustStoreUInt(0x5fcc3d14, 32). // Opcode for NFT transfer
MustStoreUInt(0, 64). // query_id
MustStoreAddr(destinationAddress). // new_owner
MustStoreAddr(walletAddress). // response_destination for excesses
MustStoreBoolBit(false). // we do not have custom_payload
MustStoreBigCoins(tlb.MustFromTON("0.01").NanoTON()). // forward_amount
MustStoreBoolBit(true). // we store forward_payload as a reference
MustStoreRef(forwardPayload). // store forward_payload as a reference
EndCell()
internalMessage := cell.BeginCell().
MustStoreUInt(0x18, 6). // bounce
MustStoreAddr(nftAddress).
MustStoreBigCoins(tlb.MustFromTON("0.05").NanoTON()).
MustStoreUInt(1, 1 + 4 + 4 + 64 + 32 + 1 + 1). // We store 1 that means we have body as a reference
MustStoreRef(transferNftBody).
EndCell()
Опкод передачи NFT взят из того же самого стандарта. Теперь давайте завершим сообщение, как было изложено в предыдущих разделах этого руководства. Корректный код, необходимый для завершения сообщения, находится в репозитории GitHub.
Такую же процедуру можно выполнить и с жетонами. Подробнее читайте в TL-B-стандарте о переводе жетонов jetton-standart. На данный момент существует небольшая разница между передачей NFT и жетонов.
GET-методы кошелька V3 и V4
Смарт-контракты часто используют GET-методы, однако они работают на стороне клиента, а не внутри блокчейна. GET-методы имеют множество применений и обеспечивают смарт-контрактам доступ к различным типам данных. Например, метод get_nft_data() в смарт-контрактах NFT позволяет пользователям получать информацию о владельце, коллекции NFT, содержимом content
.
Ниже мы познакомимся с основными GET-методами, используемыми в V3 и V4. Начнем с методов, которые одинаковы для обеих вер сий кошелька:
Метод | Описание |
---|---|
int seqno() | Необходим для получения текущего seqno и отправки сообщений с корректным значением. Этот метод вызывался довольно часто в предыдущих разделах руководства. |
int get_public_key() | Используется для получения публичного ключа. Метод get_public_key() не имеет широкого применения и может использоваться различными сервисами. Например, некоторые API-сервисы позволяют получить множество кошельков с одним и тем же открытым ключом |
Теперь давайте перейдем к методам, которые использует только кошелек V4:
Метод | Описание |
---|---|
int get_subwallet_id() | Этот метод мы уже рассматривали ранее в руководстве, он позволяет получить subwallet_id – идентификатор subwallet |
int is_plugin_installed(int wc, int addr_hash) | Нужен для передачи в блокчейн данных об установке плагина. Для вызова этого метода необходимо передать воркчейн и хэш адреса плагина |
tuple get_plugin_list() | Возвращает адрес установленных плагинов |
Давайте рассмотрим методы get_public_key
и is_plugin_installed
. Эти два метода были выбраны потому, что сначала нужно извлечь публичный ключ из данных длиной 256 бит, а затем научиться передавать slice – срез полученных данных, а также различные типы данных в GET-методы. Это очень удобно и должно помочь в правильном использовании данных методов.
Сначала нам нужен клиент, способный отправлять запросы. Поэтому в качестве примера мы будем использовать адрес конкретного кошелька (EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF):
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
import { Address } from '@ton/core';
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
apiKey: "put your api key" // you can get an api key from @tonapibot bot in Telegram
});
const walletAddress = Address.parse("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF"); // my wallet address as an example
import (
"context"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"log"
)
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection)
block, err := client.CurrentMasterchainInfo(context.Background()) // get current block, we will need it in requests to LiteServer
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
walletAddress := address.MustParseAddr("EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF") // my wallet address as an example
Теперь нам нужно вызвать GET-метода кошелька.
- JavaScript
- Golang
// I always call runMethodWithError instead of runMethod to be able to check the exit_code of the called method.
let getResult = await client.runMethodWithError(walletAddress, "get_public_key"); // run get_public_key GET Method
const publicKeyUInt = getResult.stack.readBigNumber(); // read answer that contains uint256
const publicKey = publicKeyUInt.toString(16); // get hex string from bigint (uint256)
console.log(publicKey)
getResult, err := client.RunGetMethod(context.Background(), block, walletAddress, "get_public_key") // run get_public_key GET Method
if err != nil {
log.Fatalln("RunGetMethod err:", err.Error())
return
}
// We have a response as an array with values and should specify the index when reading it
// In the case of get_public_key, we have only one returned value that is stored at 0 index
publicKeyUInt := getResult.MustInt(0) // read answer that contains uint256
publicKey := publicKeyUInt.Text(16) // get hex string from bigint (uint256)
log.Println(publicKey)
Успешным завершением вызова будет очень большое 256-битное число, которое необходимо перевести в строку шестнадцатеричного формата. Результирующая строка для адреса кошелька, который мы указали выше, выглядит следующим образом: 430db39b13cf3cb76bfa818b6b13417b82be2c6c389170fbe06795c71996b1f8
.
Далее используем TonAPI (метод /v1/wallet/findByPubkey), вводя полученную шестнадцатеричную строку в систему, и сразу становится ясно, что первый элемент массива в ответе будет идентифицировать наш кошелек.
Затем переходим к методу is_plugin_installed
. В качестве примера мы будем снова использовать кошелек, который использовали ранее (EQAM7M--HGyfxlErAIUODrxBA3yj5roBeYiTuy6BHgJ3Sx8k) и плагин (EQBTKTis-SWYdupy99ozeOvnEBu8LRrQP_N9qwOTSAy3sQSZ):
- JavaScript
- Golang
const oldWalletAddress = Address.parse("EQAM7M--HGyfxlErAIUODrxBA3yj5roBeYiTuy6BHgJ3Sx8k"); // my old wallet address
const subscriptionAddress = Address.parseFriendly("EQBTKTis-SWYdupy99ozeOvnEBu8LRrQP_N9qwOTSAy3sQSZ"); // subscription plugin address which is already installed on the wallet
oldWalletAddress := address.MustParseAddr("EQAM7M--HGyfxlErAIUODrxBA3yj5roBeYiTuy6BHgJ3Sx8k")
subscriptionAddress := address.MustParseAddr("EQBTKTis-SWYdupy99ozeOvnEBu8LRrQP_N9qwOTSAy3sQSZ") // subscription plugin address which is already installed on the wallet
Теперь нужно получить хэш-адрес плагина, чтобы перевести его в число и отправить в GET-метод.
- JavaScript
- Golang
const hash = BigInt(`0x${subscriptionAddress.address.hash.toString("hex")}`) ;
getResult = await client.runMethodWithError(oldWalletAddress, "is_plugin_installed",
[
{type: "int", value: BigInt("0")}, // pass workchain as int
{type: "int", value: hash} // pass plugin address hash as int
]);
console.log(getResult.stack.readNumber()); // -1
import (
"math/big"
)
hash := big.NewInt(0).SetBytes(subscriptionAddress.Data())
// runGetMethod will automatically identify types of passed values
getResult, err = client.RunGetMethod(context.Background(), block, oldWalletAddress,
"is_plugin_installed",
0, // pass workchain
hash) // pass plugin address
if err != nil {
log.Fatalln("RunGetMethod err:", err.Error())
return
}
log.Println(getResult.MustInt(0)) // -1
Ответ должен быть -1
, что будет означать, что результат истина. При необходимости можно также передать только срез и ячейку. Для этого достаточно создать Slice
или Cell
и передать их вместо использования BigInt, указав соответствующий тип.
Развертывание контракта через кошелек
В предыдущей главе мы развернули кошелек: для этого мы отправили несколько TON, а затем сообщение из кошелька – для развертывания смарт-контракта. Однако этот процесс не очень широко используется с внешними сообщениями и часто применяется только для кошельков. При разработке контрактов процесс развертывания инициализируется путем отправки внутренних сообщений.
Для этого мы воспользуемся смарт-контрактом кошелька V3R2, который использовался в предыдущей главе.
В этом случае для subwallet_id
нужно установить значение 3
или любое другое число, необходимое для получения другого адреса при использовании того же приватного ключа (его можно менять):
- JavaScript
- Golang
import { beginCell, Cell } from '@ton/core';
import { mnemonicToWalletKey } from '@ton/crypto';
const mnemonicArray = 'put your mnemonic'.split(" ");
const keyPair = await mnemonicToWalletKey(mnemonicArray); // extract private and public keys from mnemonic
const codeCell = Cell.fromBase64('te6ccgEBCAEAhgABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQCW8oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOgwAaTIyx/LH8v/ye1UAATQMAIBSAYHABe7Oc7UTQ0z8x1wv/gAEbjJftRNDXCx+A==');
const dataCell = beginCell()
.storeUint(0, 32) // Seqno
.storeUint(3, 32) // Subwallet ID
.storeBuffer(keyPair.publicKey) // Public Key
.endCell();
const stateInit = beginCell()
.storeBit(0) // No split_depth
.storeBit(0) // No special
.storeBit(1) // We have code
.storeRef(codeCell)
.storeBit(1) // We have data
.storeRef(dataCell)
.storeBit(0) // No library
.endCell();
import (
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"github.com/xssnick/tonutils-go/tvm/cell"
"golang.org/x/crypto/pbkdf2"
"strings"
)
mnemonicArray := strings.Split("put your mnemonic", " ")
// The following three lines will extract the private key using the mnemonic phrase.
// We will not go into cryptographic details. In the library tonutils-go, it is all implemented,
// but it immediately returns the finished object of the wallet with the address and ready-made methods.
// So we’ll have to write the lines to get the key separately. Goland IDE will automatically import
// all required libraries (crypto, pbkdf2 and others).
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonicArray, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
// 32 is a key len
privateKey := ed25519.NewKeyFromSeed(k) // get private key
publicKey := privateKey.Public().(ed25519.PublicKey) // get public key from private key
BOCBytes, _ := base64.StdEncoding.DecodeString("te6ccgEBCAEAhgABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQCW8oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOgwAaTIyx/LH8v/ye1UAATQMAIBSAYHABe7Oc7UTQ0z8x1wv/gAEbjJftRNDXCx+A==")
codeCell, _ := cell.FromBOC(BOCBytes)
dataCell := cell.BeginCell().
MustStoreUInt(0, 32). // Seqno
MustStoreUInt(3, 32). // Subwallet ID
MustStoreSlice(publicKey, 256). // Public Key
EndCell()
stateInit := cell.BeginCell().
MustStoreBoolBit(false). // No split_depth
MustStoreBoolBit(false). // No special
MustStoreBoolBit(true). // We have code
MustStoreRef(codeCell).
MustStoreBoolBit(true). // We have data
MustStoreRef(dataCell).
MustStoreBoolBit(false). // No library
EndCell()
Далее мы получим адрес из нашего контракта и создадим InternalMessage. Также добавим комментарий "Deploying..." к нашему сообще нию.
- JavaScript
- Golang
import { Address, toNano } from '@ton/core';
const contractAddress = new Address(0, stateInit.hash()); // get the hash of stateInit to get the address of our smart contract in workchain with ID 0
console.log(`Contract address: ${contractAddress.toString()}`); // Output contract address to console
const internalMessageBody = beginCell()
.storeUint(0, 32)
.storeStringTail('Deploying...')
.endCell();
const internalMessage = beginCell()
.storeUint(0x10, 6) // no bounce
.storeAddress(contractAddress)
.storeCoins(toNano('0.01'))
.storeUint(0, 1 + 4 + 4 + 64 + 32)
.storeBit(1) // We have State Init
.storeBit(1) // We store State Init as a reference
.storeRef(stateInit) // Store State Init as a reference
.storeBit(1) // We store Message Body as a reference
.storeRef(internalMessageBody) // Store Message Body Init as a reference
.endCell();
import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"log"
)
contractAddress := address.NewAddress(0, 0, stateInit.Hash()) // get the hash of stateInit to get the address of our smart contract in workchain with ID 0
log.Println("Contract address:", contractAddress.String()) // Output contract address to console
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32).
MustStoreStringSnake("Deploying...").
EndCell()
internalMessage := cell.BeginCell().
MustStoreUInt(0x10, 6). // no bounce
MustStoreAddr(contractAddress).
MustStoreBigCoins(tlb.MustFromTON("0.01").NanoTON()).
MustStoreUInt(0, 1+4+4+64+32).
MustStoreBoolBit(true). // We have State Init
MustStoreBoolBit(true). // We store State Init as a reference
MustStoreRef(stateInit). // Store State Init as a reference
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(internalMessageBody). // Store Message Body Init as a reference
EndCell()
Обратите внимание, что выше были указаны биты, а stateInit
и internalMessageBody
были сохранены как ссылки. Поскольку ссылки хранятся отдельно, мы можем написать 4 (0b100) + 2 (0b10) + 1 (0b1) -> (4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
, что означает (0b111, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
, а затем сохранить две ссылки.
Далее подготовим сообщение для нашего кошелька и отправим его:
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
import { sign } from '@ton/crypto';
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: 'put your api key' // you can get an api key from @tonapibot bot in Telegram
});
const walletMnemonicArray = 'put your mnemonic'.split(' ');
const walletKeyPair = await mnemonicToWalletKey(walletMnemonicArray); // extract private and public keys from mnemonic
const walletAddress = Address.parse('put your wallet address with which you will deploy');
const getMethodResult = await client.runMethod(walletAddress, 'seqno'); // run "seqno" GET method from your wallet contract
const seqno = getMethodResult.stack.readNumber(); // get seqno from response
// message for our wallet
const toSign = beginCell()
.storeUint(698983191, 32) // subwallet_id
.storeUint(Math.floor(Date.now() / 1e3) + 60, 32) // Message expiration time, +60 = 1 minute
.storeUint(seqno, 32) // store seqno
// Do not forget that if we use Wallet V4, we need to add .storeUint(0, 8)
.storeUint(3, 8)
.storeRef(internalMessage);
const signature = sign(toSign.endCell().hash(), walletKeyPair.secretKey); // get the hash of our message to wallet smart contract and sign it to get signature
const body = beginCell()
.storeBuffer(signature) // store signature
.storeBuilder(toSign) // store our message
.endCell();
const external = beginCell()
.storeUint(0b10, 2) // indicate that it is an incoming external message
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress)
.storeCoins(0) // Import fee
.storeBit(0) // We do not have State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(body) // Store Message Body as a reference
.endCell();
console.log(external.toBoc().toString('base64'));
client.sendFile(external.toBoc());
import (
"context"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/tl"
"github.com/xssnick/tonutils-go/ton"
"time"
)
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection)
block, err := client.CurrentMasterchainInfo(context.Background()) // get current block, we will need it in requests to LiteServer
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
walletMnemonicArray := strings.Split("put your mnemonic", " ")
mac = hmac.New(sha512.New, []byte(strings.Join(walletMnemonicArray, " ")))
hash = mac.Sum(nil)
k = pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
// 32 is a key len
walletPrivateKey := ed25519.NewKeyFromSeed(k) // get private key
walletAddress := address.MustParseAddr("put your wallet address with which you will deploy")
getMethodResult, err := client.RunGetMethod(context.Background(), block, walletAddress, "seqno") // run "seqno" GET method from your wallet contract
if err != nil {
log.Fatalln("RunGetMethod err:", err.Error())
return
}
seqno := getMethodResult.MustInt(0) // get seqno from response
toSign := cell.BeginCell().
MustStoreUInt(698983191, 32). // subwallet_id | We consider this further
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32). // message expiration time, +60 = 1 minute
MustStoreUInt(seqno.Uint64(), 32). // store seqno
// Do not forget that if we use Wallet V4, we need to add MustStoreUInt(0, 8).
MustStoreUInt(3, 8). // store mode of our internal message
MustStoreRef(internalMessage) // store our internalMessage as a reference
signature := ed25519.Sign(walletPrivateKey, toSign.EndCell().Hash()) // get the hash of our message to wallet smart contract and sign it to get signature
body := cell.BeginCell().
MustStoreSlice(signature, 512). // store signature
MustStoreBuilder(toSign). // store our message
EndCell()
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // ext_in_msg_info$10
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress). // Destination address
MustStoreCoins(0). // Import Fee
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(body). // Store Message Body as a reference
EndCell()
var resp tl.Serializable
err = client.Client().QueryLiteserver(context.Background(), ton.SendMessage{Body: externalMessage.ToBOCWithFlags(false)}, &resp)
if err != nil {
log.Fatalln(err.Error())
return
}
На этом мы завершаем работу с обычными кошельками. На данном этапе вы должны хорошо понимать, как взаимодействовать со смарт-контрактами кошелька, отправлять сообщения и уметь использовать различные типы библиотек.
🔥 Highload Wallet V3
При работе с большим количеством сообщений за короткий промежуток времени возникает необходимость в специальном кошельке, который называется Highload Wallet (highload-кошелек). Highload-кошелек V2 долгое время был основным кошельком на TON, но его использование требует осторожности, так как вы можете заблокировать все средства.
С появлением highload-кошелька V3 этот вопрос был решен на уровне архитектуры, было снижено потребление газа. В этом разделе мы рассмотрим основы highload-кошелька V3 и важные нюансы, о которых следует помнить.
Мы будем работать с немного измененной версией обертки для контракта, поскольку в ней есть защита от некоторых неочевидных ошибок.