# TinyGo SDK Руководство по написанию DChain smart contracts на Go с использованием TinyGo. ## Установка TinyGo ```bash # macOS brew install tinygo # Linux (deb) wget https://github.com/tinygo-org/tinygo/releases/download/v0.32.0/tinygo_0.32.0_amd64.deb sudo dpkg -i tinygo_0.32.0_amd64.deb # Windows # Скачать installer с https://github.com/tinygo-org/tinygo/releases # Проверка tinygo version ``` Требуется TinyGo **0.30+** (поддержка `//go:wasmimport` + target wasip1). ## Структура контракта ``` contracts/ mycontract/ main.go # контракт mycontract_abi.json # ABI mycontract.wasm # собранный (gitignore или коммитим?) ``` Минимальный контракт: ```go package main import dc "go-blockchain/contracts/sdk" //export my_method func myMethod() { dc.Log("hello from Go contract!") } // main обязателен для TinyGo, но не вызывается. func main() {} ``` Каждая `//export ` функция становится вызываемым методом контракта. ## SDK API Импорт: `import dc "go-blockchain/contracts/sdk"` ### Аргументы ```go // Строковый аргумент по индексу (maxLen — максимальная длина буфера) name := dc.ArgStr(0, 64) // args[0] as string addr := dc.ArgStr(1, 128) // args[1] as string // Числовой аргумент (uint64) amount := dc.ArgU64(0) // args[0] as uint64 ``` ### State ```go // Читать/писать произвольные байты data := dc.GetState("key") // []byte или nil dc.SetState("key", []byte("value")) // записать dc.SetState("key", nil) // удалить // Удобные string обёртки s := dc.GetStateStr("owner") // string (пустой если нет) dc.SetStateStr("owner", caller) // записать string // uint64 (хранится как 8-byte big-endian) count := dc.GetU64("counter") // uint64 dc.PutU64("counter", count+1) // записать ``` ### Идентификация ```go caller := dc.Caller() // hex pubkey вызывающего (64 символа) height := dc.BlockHeight() // uint64, текущий блок treasury := dc.Treasury() // hex адрес treasury контракта ``` ### Токены ```go // Баланс адреса в µT bal := dc.Balance(pubkey) // uint64 // Перевод µT ok := dc.Transfer(from, to, amount) // bool: true=success ``` `Transfer` в контексте контракта: `from` должен быть либо caller'ом, либо `dc.Treasury()` — только так контракт может тратить чужие деньги. ### Межконтрактные вызовы ```go // Вызвать метод другого контракта ok := dc.CallContract(contractID, "method", `["arg1","arg2"]`) // Без аргументов ok := dc.CallContract(contractID, "get_price", "[]") ``` Подробнее: [Межконтрактные вызовы](inter-contract.md) ### Логирование ```go dc.Log("message") // строка видна в Explorer → Logs dc.Log("value: " + strconv.FormatUint(v, 10)) ``` ## Паттерны ### Owner-только метод ```go //export admin_action func adminAction() { owner := dc.GetStateStr("owner") if owner == "" { // первый вызов — устанавливаем owner dc.SetStateStr("owner", dc.Caller()) dc.Log("initialized") return } if dc.Caller() != owner { dc.Log("unauthorized") return } // ... логика ... } ``` ### Платный метод (fee в treasury) ```go //export register func register() { name := dc.ArgStr(0, 64) if name == "" { return } const fee = 1000 // µT caller := dc.Caller() treasury := dc.Treasury() // Снять fee с caller и положить в treasury if !dc.Transfer(caller, treasury, fee) { dc.Log("insufficient balance") return } dc.SetStateStr("reg:"+name, caller) dc.Log("registered: " + name) } ``` ### Эскроу с release ```go //export lock func lock() { id := dc.ArgStr(0, 64) amount := dc.ArgU64(1) buyer := dc.Caller() treasury := dc.Treasury() if !dc.Transfer(buyer, treasury, amount) { dc.Log("transfer failed") return } dc.SetStateStr("buyer:"+id, buyer) dc.PutU64("amount:"+id, amount) dc.Log("locked: " + id) } //export release func release() { id := dc.ArgStr(0, 64) buyer := dc.GetStateStr("buyer:" + id) if dc.Caller() != buyer { dc.Log("unauthorized") return } seller := dc.ArgStr(1, 128) amount := dc.GetU64("amount:" + id) dc.Transfer(dc.Treasury(), seller, amount) dc.Log("released: " + id) } ``` ## Сборка ```bash cd contracts/mycontract # Собрать WASM tinygo build -o mycontract.wasm -target wasip1 -no-debug . # Проверить размер ls -lh mycontract.wasm # Опционально: wasm-strip для уменьшения размера wasm-strip mycontract.wasm ``` Флаги: - `-target wasip1` — WASI Preview 1 (единственный поддерживаемый target) - `-no-debug` — убрать debug info из WASM (уменьшает размер) - `-opt=2` — агрессивная оптимизация (по умолчанию в release) ## Деплой ```bash CONTRACT_ID=$(client deploy-contract \ --key /keys/node1.json \ --wasm mycontract.wasm \ --abi mycontract_abi.json \ --node http://localhost:8081 | grep contract_id | awk '{print $2}') echo "Contract: $CONTRACT_ID" ``` ## Проверка WASM Перед деплоем можно проверить что модуль валиден: ```go // В Go тесте ctx := context.Background() v := vm.NewVM(ctx) defer v.Close(ctx) wasmBytes, _ := os.ReadFile("mycontract.wasm") if err := v.Validate(ctx, wasmBytes); err != nil { log.Fatal(err) } ``` ## Пример: hello_go Полный пример TinyGo-контракта с комментариями находится в [`contracts/hello_go/main.go`](../../contracts/hello_go/main.go). Демонстрирует: - `increment` / `get` / `reset` — счётчик с owner ACL - `greet` — строковые аргументы - `ping` — межконтрактный вызов ```bash # Собрать cd contracts/hello_go tinygo build -o hello_go.wasm -target wasip1 -no-debug . # Задеплоить docker exec node1 client deploy-contract \ --key /keys/node1.json \ --wasm hello_go.wasm \ --abi hello_go_abi.json \ --node http://node1:8080 # Вызвать docker exec node1 client call-contract \ --key /keys/node1.json --contract $HELLO_ID \ --method increment --gas 5000 --node http://node1:8080 docker exec node1 client call-contract \ --key /keys/node1.json --contract $HELLO_ID \ --method greet --arg "DChain" --gas 5000 --node http://node1:8080 ``` ## Ограничения TinyGo в контекcте WASM - Нет `goroutine` в WASM (нет многопоточности) - Нет `net`, `os`, `syscall` — контракт изолирован - Нет доступа к файловой системе и сети - `fmt.Println` и `log.Printf` работают через WASI stdout — **но не логируются в blockchain**. Используйте только `dc.Log()` для логов видимых в Explorer. - Размер памяти по умолчанию: 64 KB на stack + heap управляется TinyGo GC