В tondev чате часто возникают вопросы про взаимодействие с TON с помощью популярных языков программирования, особенно часто встречаются вопросы про взаимодействие с NFT коллекциями и контрактами в целом. Поэтому для ton_learn я решил сделать 2 урока, где мы взаимодействуем с блокчейном TON с помощью некоторых скриптов, так, чтобы читатель по итогу мог легко работать со смарт-контрактами в ТОН.
Задачи следующие:
- в этом уроке мы сделаем заготовку с кошельком, которую будем использовать далее, а также разберемся как деплоить и взаимодействовать с контрактом из первого урока
- в следующем уроке будем деплоить NFT коллекцию, а также подергаем Get-методы
Для работы с TON скриптами будем использовать библиотеку для GO tonutils-go. Данная библиотека имеет отличный баланс между верхнеуровневостью и низкоуровневостью, таким образом она позволяет писать простые скрипты, но в тоже время не лишает нас различных возможностей работы с блокчейном TON.
Даже если вы не знакомы с GO я уверен, что данный урок и скрипты будут вам понятны, но на всякий случай в самом конце урока есть ссылки на материалы, которые позволят вам быстро освоиться с GO.
Хочется также отметить, что данной библиотеки хорошая документация с примерами.
Кошелек необходим нам, чтобы отправлять сообщения внутри TON (те, что приходят в recv_internal()). По сути кошелек это смарт-контракт, способный принимать внешние сообщения (те, что recv_external()) и отправлять внутренние. Поэтому прежде чем переходить к деплою смарт-контракта, сначала создадим кошелек.
Кошелек в сети TON является смарт-контрактом, чтобы задеплоить смарт-контракт в тестовую или основную сеть, нам необходимо подключиться к сети, для этого нужен её конфиг:
- конфиг тестовой сети
- конфиг основной сети(mainnet)
Взаимодействовать с сетью мы будем через лайтсерверы.
Легкий клиент (англ. lite-client) — это программное обеспечение, которое подключается к полным узлам для взаимодействия с блокчейном. Они помогают пользователям получать доступ к блокчейну и взаимодействовать с ним без необходимости синхронизации всего блокчейна.
Итак, выполним подключение:
client := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/testnet-global.config.json"
err := client.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
api := ton.NewAPIClient(client)
Получаем api лайтсервера.
Если вы посмотрите на конфиги, вы можете увидеть несколько лайтсерверов внутри, какой выбирает библиотека? - Первый с которым будет успешное соединение
Для генерации кошелька нам нужна пара публичный/приватный ключ (получать их будет с помощью Seed фразы) и структура InitialAccountWallet, соответствующая одному из доступных версий кошельков.
Seed фраза - последовательность из слов используемых для генерации ключей.
Сгенерируем seed фразу c помощью wallet.NewSeed()
и напечатаем её, чтобы можно было скопировать и использовать кошелек в будущем.
seed := wallet.NewSeed()
fmt.Println("Seed phrase:")
fmt.Println(seed)
Эту фразу можно и нужно сохранить, чтобы использовать кошелек в будущем.
Генерируем кошелек и выводим адрес.
w, err := wallet.FromSeed(api, seed, wallet.V3)
if err != nil {
log.Fatalln("FromSeed err:", err.Error())
return
}
fmt.Println(w.Address())
Подробнее про разные версии кошельков можно прочитать здесь.
В соответствии с документацией на полученный адрес необходимо отправить Toncoin. В тестовой сети для этого есть бот https://t.me/testgiver_ton_bot . По основной сети, приложу официальную страницу.
Наш кошелек готов и чтобы получить баланс, необходимо получить текущую информацию о сети (а именно текущий блок).
block, err := api.CurrentMasterchainInfo(context.Background())
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
И далее из блока получить баланс:
balance, err := w.GetBalance(context.Background(), block)
if err != nil {
log.Fatalln("GetBalance err:", err.Error())
return
}
fmt.Println(balance)
Итоговый код createwallet.go
:
package main
import (
"context"
"log"
"fmt"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/wallet"
)
func main() {
client := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/testnet-global.config.json"
err := client.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
api := ton.NewAPIClient(client)
seed := wallet.NewSeed()
fmt.Println("Seed phrase:")
fmt.Println(seed)
w, err := wallet.FromSeed(api, seed, wallet.V3)
if err != nil {
log.Fatalln("FromSeed err:", err.Error())
return
}
fmt.Println(w.Address())
block, err := api.CurrentMasterchainInfo(context.Background())
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
balance, err := w.GetBalance(context.Background(), block)
if err != nil {
log.Fatalln("GetBalance err:", err.Error())
return
}
fmt.Println(balance)
}
Прежде, чем двигаться дальше вынесем генерацию кошелька по сид фразе в отдельную функцию.
Так как у нас уже есть сид фраза, нам не надо её больше генерировать, остается только собрать кошелек.
func getWallet(api *ton.APIClient) *wallet.Wallet {
words := strings.Split("write your seed phrase here", " ")
w, err := wallet.FromSeed(api, words, wallet.V3)
if err != nil {
panic(err)
}
return w
}
Пример с генерацией кошелька с подобной функцией есть в отдельном файле walletfunc.go
.
Теперь, когда у нас есть кошелек с балансом Toncoin на нем, мы можем деплоить смарт-контракты. В библиотеке tonutils-go
можно деплоить смарт-контракт в форме hexBoc. Boc это сериализованная форма смарт-контракта(bag-of-cells).
Самый простой способ перевести смарт-контракт в подобную форму — это использовать fift скрипт. Возьмем fift код из первого смарт-контракта и напишем скрипт, который переведет его в hexBoc.
"Asm.fif" include
// automatically generated from `C:\Users\7272~1\AppData\Local\toncli\toncli\func-libs\stdlib-tests.func` `C:\Users\7272~1\Documents\chain\firsttest\wallet\func\code.func`
PROGRAM{
DECLPROC recv_internal
128253 DECLMETHOD get_total
recv_internal PROC:<{
// in_msg_body
DUP // in_msg_body in_msg_body
SBITS // in_msg_body _2
32 LESSINT // in_msg_body _4
35 THROWIF
32 LDU // _24 _23
DROP // n
c4 PUSH // n _11
CTOS // n ds
64 LDU // n _26 _25
DROP // n total
SWAP // total n
ADD // total
NEWC // total _18
64 STU // _20
ENDC // _21
c4 POP
}>
get_total PROC:<{
//
c4 PUSH // _1
CTOS // ds
64 LDU // _8 _7
DROP // total
}>
}END>c
Если вы проходили первый урок, то Fift код контракта лежит в папке fift
Теперь скрипт, который переведет код в формат hexBOC:
#!/usr/bin/fift -s
"TonUtil.fif" include
"Asm.fif" include
."first contract:" cr
"first.fif" include
2 boc+>B dup Bx. cr cr
Подробно останавливаться на fift не будет, это выходит, за рамки этого урока, отмечу только:
- boc+>B - сериализует в формат boc
- cr - выводит в строку значение
Запустить скрипт можно либо с помощью знакомого нам toncli, а именно
toncli fift run
, либо как описано здесь.
Пример скрипта, находится в файле print-hex.fif
.
По итогу мы получим:
B5EE9C72410104010038000114FF00F4A413F4BCF2C80B0102016202030032D020D749C120F263D31F30ED44D0D33F3001A0C8CB3FC9ED540011A1E9FBDA89A1A67E61A6614973
Берем нашу заготовку с кошельком walletfunc.go
из неё сделаем скрипт деплоя контракта. Первое что сделаем, это добавим фукнциюgetContractCode()
, которая будет преобразовывать hexBOC полученный ранее в байты:
func getContractCode() *cell.Cell {
var hexBOC = "B5EE9C72410104010038000114FF00F4A413F4BCF2C80B0102016202030032D020D749C120F263D31F30ED44D0D33F3001A0C8CB3FC9ED540011A1E9FBDA89A1A67E61A6614973"
codeCellBytes, _ := hex.DecodeString(hexBOC)
codeCell, err := cell.FromBOC(codeCellBytes)
if err != nil {
panic(err)
}
return codeCell
}
Для деплоя смарт-контракта, нам необходимо сформировать StateInit
. StateInit
это комбинация кода смарт-контракта, который у нас уже есть и данных смарт-контракта. Данные смарт-контракта, это то, что мы хотим положить в регистр с4
, часто туда кладется адрес владельца смарт-контракта, для управления им. Примеры вы могли видеть в 9 и 10 уроках, где в с4
хранился владелец NFT коллекции или Jetton. В нашем примере мы можем туда положить 0 или любое число, главное 64 бит, чтобы он было 64 бит, для корректной работы логики контракта. Для данных сделаем отдельную функцию:
func getContractData() *cell.Cell {
data := cell.BeginCell().MustStoreUInt(2, 64).EndCell()
return data
}
Их StateInit благодаря хэшированию вычисляется адрес смарт-контракта.
На полученный адрес необходимо отправить сообщение и что важно не забыть про небольшое количество TON, так как смарт-контракты должны иметь положительный баланс, чтобы иметь возможность платить за хранение и обработку своих данных в блокчейне.
Также для сообщения необходимо подготовить некоторое тело, но оно может быть и пустым в зависимости от вашей ситуации.
В tonutils-go
вся эта логика находится внутри функции DeployContract
, вызов её в нашем случае будет выглядеть так:
msgBody := cell.BeginCell().MustStoreUInt(0, 64).EndCell()
fmt.Println("Deploying NFT collection contract to net...")
addr, err := w.DeployContract(context.Background(), tlb.MustFromTON("0.02"),
msgBody, getContractCode(), getContractData(), true)
if err != nil {
panic(err)
}
fmt.Println("Deployed contract addr:", addr.String())
Параметр true
указывает на необходимость "подождать" подтверждение отправки сообщения.
Важно отметить, что так как адрес мы получаем хэширование, то задеплоить два раза один и тот же контракт с одинаковыми данными не получиться, сообщение просто придет в уже существующий контракт.
Итоговый код deploycontract.go
:
package main
import (
"context"
"log"
"fmt"
"encoding/hex"
"strings"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/wallet"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/tvm/cell"
)
func main() {
client := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/testnet-global.config.json"
err := client.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
api := ton.NewAPIClient(client)
w := getWallet(api)
fmt.Println(w.Address())
block, err := api.CurrentMasterchainInfo(context.Background())
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
balance, err := w.GetBalance(context.Background(), block)
if err != nil {
log.Fatalln("GetBalance err:", err.Error())
return
}
fmt.Println(balance)
msgBody := cell.BeginCell().MustStoreUInt(0, 64).EndCell()
fmt.Println("Deploying NFT collection contract to net...")
addr, err := w.DeployContract(context.Background(), tlb.MustFromTON("0.02"),
msgBody, getContractCode(), getContractData(), true)
if err != nil {
panic(err)
}
fmt.Println("Deployed contract addr:", addr.String())
}
func getWallet(api *ton.APIClient) *wallet.Wallet {
words := strings.Split("write your seed phrase here", " ")
w, err := wallet.FromSeed(api, words, wallet.V3)
if err != nil {
panic(err)
}
return w
}
func getContractCode() *cell.Cell {
var hexBOC = "B5EE9C72410104010038000114FF00F4A413F4BCF2C80B0102016202030032D020D749C120F263D31F30ED44D0D33F3001A0C8CB3FC9ED540011A1E9FBDA89A1A67E61A6614973"
codeCellBytes, _ := hex.DecodeString(hexBOC)
codeCell, err := cell.FromBOC(codeCellBytes)
if err != nil {
panic(err)
}
return codeCell
}
func getContractData() *cell.Cell {
data := cell.BeginCell().MustStoreUInt(2, 64).EndCell()
return data
}
Теперь давайте протестируем наш смарт-контракт, а именно отправим сообщение, после которого контракт должен будет сложить его с числом в регистре с4 и сохранить получившееся значение. Возьмем нашу заготовку с кошельком walletfunc.go
и в неё добавим код отправки сообщения:
fmt.Println("Let's send message")
err = w.Send(context.Background(), &wallet.Message{
Mode: 3,
InternalMessage: &tlb.InternalMessage{
IHRDisabled: true,
Bounce: true,
DstAddr: address.MustParseAddr("your contract address"),
Amount: tlb.MustFromTON("0.05"),
Body: cell.BeginCell().MustStoreUInt(11, 32).EndCell(),
},
}, true)
if err != nil {
fmt.Println(err)
}
Схема сообщения все та же, что и раньше) Подробнее разобрано в 3 уроке. Отправляем мы сообщение от нашего кошелька.
Теперь осталось проверить про суммировались ли значения в смарт-контракте. Для этого в tonutils-go есть RunGetMethod(), в который надо передать текущий блок, адрес смарт-контракта, метод и параметры для методы.
fmt.Println("Get Method")
addr := address.MustParseAddr("your contract address")
// run get method
res, err := api.RunGetMethod(context.Background(), block, addr, "get_total")
if err != nil {
// if contract exit code != 0 it will be treated as an error too
panic(err)
}
fmt.Println(res)
Важно отметить, если вы отправите сообщение и вызовите Get контракт подряд, данные могут не успеть обновиться в блокчейне и вы можете получить старое значение. Поэтому добавляем между отправкой сообщений и Get методом, получение нового блока. И time.Sleep. Либо комментируем отправку сообщения и отдельно вызываем get метод).
В TON блоки обновляются 5 секунд.
Примерный код, находится в файле sendandget.go
В следующем уроке мы будем деплоить nft коллекцию. Также хотел отметить, что у tonutil-go на их странице есть адрес для донатов.
Собрал здесь пару ссылок, который ускорят ваше понимает скриптов из данного урока.
https://gobyexample.com/hello-world
https://learnxinyminutes.com/docs/go/
https://codesource.io/how-to-install-github-packages-in-golang/