高级
Day 8
50 分钟
智能合约调用
学习如何调用已部署的智能合约函数、获取返回值并解码结果。
学习目标
- 编码合约函数调用参数
- 创建 ContractCallTx 交易
- 签名并广播调用交易
- 获取交易执行结果
- 解码函数返回值
- 理解 Dry Run(模拟调用)
1. 合约调用类型
| 调用类型 | 消耗 Gas | 修改状态 | 适用场景 |
|---|---|---|---|
| On-chain Call | 是 | 可以 | stateful 函数调用(写操作) |
| Dry Run | 否 | 不可以 | 只读函数查询(读操作) |
| Static Call | 否 | 不可以 | 纯函数调用,无状态访问 |
最佳实践:对于只读函数(如
get()),优先使用 Dry Run,避免消耗 Gas。
2. 编码函数调用参数
准备工作
package main
import (
"fmt"
"log"
"math/big"
"github.com/aeternity/aepp-sdk-go/v9/naet"
"github.com/aeternity/aepp-sdk-go/v9/account"
"github.com/aeternity/aepp-sdk-go/v9/config"
"github.com/aeternity/aepp-sdk-go/v9/transactions"
)
func main() {
// 连接节点和编译器
node := naet.NewNode("https://testnet.aeternity.io", false)
compiler := naet.NewCompiler("https://compiler.aepps.com", false)
// 加载调用者账户
alice, _ := account.FromHexString("YOUR_PRIVATE_KEY_HEX")
// Day 7 部署的合约 ID
contractID := "ct_YOUR_CONTRACT_ID"
// 合约源码(需要与部署时相同)
sourceCode := `
@compiler >= 6
contract Counter =
record state = { count : int }
entrypoint init(initial : int) : state = { count = initial }
entrypoint get() : int = state.count
stateful entrypoint inc() = put(state{ count = state.count + 1 })
stateful entrypoint add(n : int) = put(state{ count = state.count + n })
`
编码函数调用
// 调用无参数的 get() 函数
getCalldata, err := compiler.EncodeCalldata(sourceCode, "get", []string{})
if err != nil {
log.Fatal("Encode get() failed:", err)
}
// 调用无参数的 inc() 函数
incCalldata, err := compiler.EncodeCalldata(sourceCode, "inc", []string{})
if err != nil {
log.Fatal("Encode inc() failed:", err)
}
// 调用带参数的 add(n : int) 函数
// 参数 n = 10
addCalldata, err := compiler.EncodeCalldata(sourceCode, "add", []string{"10"})
if err != nil {
log.Fatal("Encode add() failed:", err)
}
3. 发送合约调用交易
调用 stateful 函数
// 创建 TTLNoncer
ttlnoncer := transactions.NewTTLNoncer(node)
// 调用参数
amount := big.NewInt(0) // 不向合约转账
gasLimit := big.NewInt(50000) // 调用比部署消耗更少 gas
gasPrice := big.NewInt(1000000000)
abiVersion := uint16(3) // FATE ABI
// 创建合约调用交易 - 调用 inc()
callTx, err := transactions.NewContractCallTx(
alice.Address, // 调用者
contractID, // 合约 ID
amount, // 转账金额
gasLimit, // Gas 限制
gasPrice, // Gas 价格
abiVersion, // ABI 版本
incCalldata, // 编码后的调用数据
ttlnoncer, // Nonce/TTL 管理
)
if err != nil {
log.Fatal("Create call tx failed:", err)
}
// 签名交易
signedTx, txHash, _, err := transactions.SignHashTx(
alice, callTx, config.NetworkIDTestnet,
)
if err != nil {
log.Fatal("Sign failed:", err)
}
// 广播交易
err = node.PostTransaction(signedTx, txHash)
if err != nil {
log.Fatal("Broadcast failed:", err)
}
fmt.Printf("Call tx broadcasted: %s\n", txHash)
等待交易确认
import "time"
// 简单等待(生产环境应轮询检查)
fmt.Println("Waiting for confirmation...")
time.Sleep(15 * time.Second)
// 检查交易是否在链上
_, err = node.GetTransactionByHash(txHash)
if err != nil {
log.Fatal("Transaction not found:", err)
}
fmt.Println("Transaction confirmed!")
4. 获取并解码返回值
获取交易执行信息
// 获取交易详细信息(包含调用结果)
txInfo, err := node.GetTransactionInfoByHash(txHash)
if err != nil {
log.Fatal("Get tx info failed:", err)
}
// 检查调用是否成功
if txInfo.CallInfo.ReturnType != "ok" {
log.Fatalf("Call failed: %s - %v",
txInfo.CallInfo.ReturnType,
txInfo.CallInfo.ReturnVal)
}
// 获取编码的返回值
encodedResult := txInfo.CallInfo.ReturnVal
fmt.Printf("Encoded result: %s\n", encodedResult)
// Gas 消耗统计
fmt.Printf("Gas used: %d\n", txInfo.CallInfo.GasUsed)
解码返回值
// 使用编译器解码返回值
// 参数:编码结果、状态("ok"/"revert")、函数名、源码
result, err := compiler.DecodeCallResult(
encodedResult, // 编码的返回值
"ok", // 调用状态
"inc", // 函数名
sourceCode, // 合约源码
)
if err != nil {
log.Fatal("Decode failed:", err)
}
fmt.Printf("Function returned: %v\n", result)
// inc() 返回 unit (空)
// 解码 get() 函数返回值(返回 int)
getResult, _ := compiler.DecodeCallResult(
getEncodedResult, "ok", "get", sourceCode,
)
fmt.Printf("Current count: %v\n", getResult)
常见返回类型
| Sophia 类型 | Go 解码类型 | 示例 |
|---|---|---|
int | *big.Int | 42 |
string | string | "hello" |
bool | bool | true |
address | string | "ak_..." |
unit | 空接口{} | {} |
list(int) | []interface{} | [1, 2, 3] |
option(T) | map 或 nil | {"Some": 42} |
5. Dry Run 模拟调用
Dry Run 允许在不消耗 Gas 的情况下模拟合约调用,适合只读查询。
Dry Run 使用场景
- 查询合约状态变量
- 调用只读(非 stateful)函数
- 在正式调用前预估 Gas 消耗
- 测试调用参数是否正确
模拟调用示例
// 构建 Dry Run 请求(具体 API 取决于 SDK 版本)
// 以下为概念示例
// 方式 1:使用节点的 debug 端点
// POST /debug/contracts/code/call
dryRunResult, err := node.DryRunContractCall(
contractID,
getCalldata, // 调用 get()
alice.Address, // 调用者(可选)
)
// 方式 2:使用低级 HTTP 请求
import (
"bytes"
"encoding/json"
"net/http"
)
type DryRunRequest struct {
Top string `json:"top"` // 区块高度
TxEvents bool `json:"txEvents"`
Calls []CallObj `json:"calls"`
}
type CallObj struct {
Contract string `json:"contract"`
Calldata string `json:"calldata"`
Amount int64 `json:"amount"`
Gas int64 `json:"gas"`
}
// 构建请求
req := DryRunRequest{
Top: "latest",
TxEvents: false,
Calls: []CallObj{{
Contract: contractID,
Calldata: getCalldata,
Amount: 0,
Gas: 50000,
}},
}
// 发送请求到节点
// POST https://testnet.aeternity.io/v3/dry-run
注意:Dry Run 的结果不会被写入区块链,状态变更是临时的。
完整调用示例
package main
import (
"fmt"
"log"
"math/big"
"time"
"github.com/aeternity/aepp-sdk-go/v9/naet"
"github.com/aeternity/aepp-sdk-go/v9/account"
"github.com/aeternity/aepp-sdk-go/v9/config"
"github.com/aeternity/aepp-sdk-go/v9/transactions"
)
func main() {
// 初始化
node := naet.NewNode("https://testnet.aeternity.io", false)
compiler := naet.NewCompiler("https://compiler.aepps.com", false)
alice, _ := account.FromHexString("YOUR_PRIVATE_KEY_HEX")
contractID := "ct_YOUR_CONTRACT_ID" // Day 7 部署的合约
sourceCode := `
@compiler >= 6
contract Counter =
record state = { count : int }
entrypoint init(initial : int) : state = { count = initial }
entrypoint get() : int = state.count
stateful entrypoint inc() = put(state{ count = state.count + 1 })
stateful entrypoint add(n : int) = put(state{ count = state.count + n })
`
ttlnoncer := transactions.NewTTLNoncer(node)
gasLimit := big.NewInt(50000)
gasPrice := big.NewInt(1000000000)
// 1. 调用 add(5)
fmt.Println("=== Calling add(5) ===")
addCalldata, _ := compiler.EncodeCalldata(sourceCode, "add", []string{"5"})
addTx, _ := transactions.NewContractCallTx(
alice.Address, contractID,
big.NewInt(0), gasLimit, gasPrice, uint16(3),
addCalldata, ttlnoncer,
)
signedTx, txHash, _, _ := transactions.SignHashTx(
alice, addTx, config.NetworkIDTestnet,
)
node.PostTransaction(signedTx, txHash)
fmt.Printf("add(5) tx: %s\n", txHash)
time.Sleep(15 * time.Second)
// 2. 获取执行结果
txInfo, err := node.GetTransactionInfoByHash(txHash)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Gas used: %d\n", txInfo.CallInfo.GasUsed)
// 3. 调用 get() 查看当前值
fmt.Println("\n=== Calling get() ===")
getCalldata, _ := compiler.EncodeCalldata(sourceCode, "get", []string{})
getTx, _ := transactions.NewContractCallTx(
alice.Address, contractID,
big.NewInt(0), gasLimit, gasPrice, uint16(3),
getCalldata, ttlnoncer,
)
signedGetTx, getTxHash, _, _ := transactions.SignHashTx(
alice, getTx, config.NetworkIDTestnet,
)
node.PostTransaction(signedGetTx, getTxHash)
time.Sleep(15 * time.Second)
getInfo, _ := node.GetTransactionInfoByHash(getTxHash)
// 4. 解码返回值
result, _ := compiler.DecodeCallResult(
getInfo.CallInfo.ReturnVal, "ok", "get", sourceCode,
)
fmt.Printf("Current count: %v\n", result)
fmt.Println("\nContract interaction complete!")
}
知识检查
- On-chain Call 和 Dry Run 的主要区别是什么?
- 如何编码调用
add(n : int)函数,参数值为 100? - 解码返回值时需要提供哪些参数?
- 什么情况下应该使用 Dry Run?
本页目录