高级 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.Int42
stringstring"hello"
boolbooltrue
addressstring"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!")
}
知识检查
  1. On-chain Call 和 Dry Run 的主要区别是什么?
  2. 如何编码调用 add(n : int) 函数,参数值为 100?
  3. 解码返回值时需要提供哪些参数?
  4. 什么情况下应该使用 Dry Run?