高级 Day 7 60 分钟

智能合约部署

学习使用 Sophia 编译器编译智能合约,并通过 ContractCreateTx 部署到链上。

学习目标
  • 理解 Sophia 智能合约基础
  • 连接和使用 Sophia 编译器
  • 编译合约获取 bytecode
  • 编码 init 构造函数调用
  • 创建并广播合约部署交易
  • 获取部署的合约 ID
1. Sophia 智能合约概述

Sophia 是 Aeternity 专用的智能合约语言,具有函数式编程特性,运行在 FATE 虚拟机上。

Sophia 语言特点
特性说明
函数式类似 Haskell/OCaml 语法风格
强类型编译时类型检查,减少运行时错误
安全无可变全局状态,避免重入攻击
FATE VM高效字节码虚拟机,Gas 消耗更低
合约基本结构
// 简单的 Identity 合约
contract Identity =
    // 状态变量(可选)
    record state = { count : int }
    
    // 构造函数(初始化状态)
    entrypoint init() = { count = 0 }
    
    // 公开函数(可被外部调用)
    entrypoint main(x : int) = x
    
    // 可修改状态的函数
    stateful entrypoint increment() = 
        put(state{ count = state.count + 1 })
        state.count
2. 配置 Sophia 编译器

部署合约前需要连接 Sophia 编译器。可以使用官方托管的编译器或本地部署。

编译器选项
选项URL说明
官方托管https://compiler.aepps.com公共编译器服务
测试网https://compiler.testnet.aeternity.io测试网专用
本地部署http://localhost:3080Docker 部署本地编译器
初始化编译器连接
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/transactions"
)

func main() {
    // 连接节点
    node := naet.NewNode("https://testnet.aeternity.io", false)
    
    // 连接编译器(第二个参数表示是否为 debug 模式)
    compiler := naet.NewCompiler("https://compiler.aepps.com", false)
    
    // 验证编译器连接
    version, err := compiler.GetCompilerVersion()
    if err != nil {
        log.Fatal("Compiler connection failed:", err)
    }
    fmt.Printf("Connected to Sophia Compiler v%s\n", version)
    
    // 加载账户
    alice, _ := account.FromHexString("YOUR_PRIVATE_KEY_HEX")
    fmt.Printf("Deployer: %s\n", alice.Address)
}
3. 编译合约代码
定义合约源码
// 定义 Sophia 合约源码
sourceCode := `
@compiler >= 6

contract Identity =
    // 状态类型定义
    record state = { owner : address, value : int }
    
    // 构造函数 - 初始化合约状态
    entrypoint init(initial_value : int) : state = 
        { owner = Call.caller, value = initial_value }
    
    // 只读函数 - 返回输入值
    entrypoint main(x : int) : int = x
    
    // 只读函数 - 获取当前值
    entrypoint get_value() : int = state.value
    
    // 状态修改函数 - 设置新值
    stateful entrypoint set_value(new_value : int) =
        require(Call.caller == state.owner, "Only owner")
        put(state{ value = new_value })
    
    // 只读函数 - 获取所有者
    entrypoint get_owner() : address = state.owner
`
编译获取字节码
// 编译合约源码
bytecode, err := compiler.CompileContract(sourceCode)
if err != nil {
    log.Fatal("Compilation failed:", err)
}
fmt.Printf("Bytecode length: %d bytes\n", len(bytecode)/2)
fmt.Printf("Bytecode prefix: %s...\n", bytecode[:64])
提示:编译后的 bytecode 是十六进制字符串,以 cb_ 开头。
4. 编码构造函数调用

每个合约都有 init 构造函数。部署时必须编码 init 调用数据。

参数编码规则
Sophia 类型Go 参数格式示例
int数字字符串"42"
string带引号的字符串"\"hello\""
address地址字符串"ak_..."
booltrue/false"true"
list(int)列表字面量"[1, 2, 3]"
option(int)Some/None"Some(42)"
编码 init 调用
// init 函数需要一个 int 参数:initial_value
// 参数以字符串数组形式传递
initArgs := []string{"100"}  // 初始值设为 100

initCalldata, err := compiler.EncodeCalldata(sourceCode, "init", initArgs)
if err != nil {
    log.Fatal("Encode init failed:", err)
}
fmt.Printf("Init calldata: %s\n", initCalldata)

// 如果 init 无参数
// initCalldata, err := compiler.EncodeCalldata(sourceCode, "init", []string{})
5. 创建部署交易
部署参数说明
参数类型说明推荐值
vmVersionuint16虚拟机版本5 (FATE)
abiVersionuint16ABI 版本3 (FATE)
deposit*big.Int押金(通常为0)0
amount*big.Int转给合约的金额0 或所需金额
gasLimit*big.Int最大 Gas 消耗100000+
gasPrice*big.IntGas 单价1000000000
创建并广播部署交易
// 创建 TTLNoncer
ttlnoncer := transactions.NewTTLNoncer(node)

// 部署参数
deposit := big.NewInt(0)                 // 无押金
amount := big.NewInt(0)                  // 不向合约转账
gasLimit := big.NewInt(200000)           // 合约部署需要较多 gas
gasPrice := big.NewInt(1000000000)       // 1 Gwei

vmVersion := uint16(5)   // FATE VM
abiVersion := uint16(3)  // FATE ABI

// 创建合约部署交易
createTx, err := transactions.NewContractCreateTx(
    alice.Address,    // 部署者地址
    bytecode,         // 编译后的字节码
    vmVersion,        // VM 版本
    abiVersion,       // ABI 版本
    deposit,          // 押金
    amount,           // 初始转账金额
    gasLimit,         // Gas 上限
    gasPrice,         // Gas 价格
    initCalldata,     // 构造函数调用数据
    ttlnoncer,        // Nonce 和 TTL 管理
)
if err != nil {
    log.Fatal("Create ContractCreateTx failed:", err)
}

// 签名交易
signedTx, txHash, _, err := transactions.SignHashTx(alice, createTx, config.NetworkIDTestnet)
if err != nil {
    log.Fatal("Sign failed:", err)
}
fmt.Printf("Transaction hash: %s\n", txHash)

// 广播交易
err = node.PostTransaction(signedTx, txHash)
if err != nil {
    log.Fatal("Broadcast failed:", err)
}
fmt.Println("Contract deployment transaction broadcasted!")
6. 获取合约 ID
计算合约 ID
// 合约 ID 可以从交易对象计算得出
// 无需等待交易确认
contractID, err := createTx.ContractID()
if err != nil {
    log.Fatal("Get contract ID failed:", err)
}
fmt.Printf("Contract ID: %s\n", contractID)
// 输出类似: ct_2JVx...

// 等待交易确认(可选)
fmt.Println("Waiting for confirmation...")
time.Sleep(15 * time.Second)

// 验证交易状态
txInfo, err := node.GetTransactionByHash(txHash)
if err != nil {
    log.Fatal("Get transaction info failed:", err)
}
fmt.Printf("Transaction block: %s\n", txInfo.BlockHash)
合约 ID 格式
前缀类型用途
ct_Contract合约地址,用于调用合约
cb_Contract Bytecode编译后的合约代码
th_Transaction Hash交易哈希
完整部署示例
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() {
    // 1. 初始化连接
    node := naet.NewNode("https://testnet.aeternity.io", false)
    compiler := naet.NewCompiler("https://compiler.aepps.com", false)
    
    // 2. 加载部署者账户
    alice, err := account.FromHexString("YOUR_PRIVATE_KEY_HEX")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Deployer: %s\n", alice.Address)

    // 3. 定义合约源码
    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 })
`

    // 4. 编译合约
    bytecode, err := compiler.CompileContract(sourceCode)
    if err != nil {
        log.Fatal("Compile error:", err)
    }
    fmt.Println("Contract compiled successfully")

    // 5. 编码 init 调用(初始值为 0)
    initCalldata, err := compiler.EncodeCalldata(sourceCode, "init", []string{"0"})
    if err != nil {
        log.Fatal("Encode init error:", err)
    }

    // 6. 创建部署交易
    ttlnoncer := transactions.NewTTLNoncer(node)
    
    createTx, err := transactions.NewContractCreateTx(
        alice.Address,
        bytecode,
        uint16(5),                    // FATE VM
        uint16(3),                    // FATE ABI
        big.NewInt(0),                // deposit
        big.NewInt(0),                // amount
        big.NewInt(200000),           // gasLimit
        big.NewInt(1000000000),       // gasPrice
        initCalldata,
        ttlnoncer,
    )
    if err != nil {
        log.Fatal("Create tx error:", err)
    }

    // 7. 签名并广播
    signedTx, txHash, _, err := transactions.SignHashTx(
        alice, createTx, config.NetworkIDTestnet,
    )
    if err != nil {
        log.Fatal("Sign error:", err)
    }

    err = node.PostTransaction(signedTx, txHash)
    if err != nil {
        log.Fatal("Broadcast error:", err)
    }
    fmt.Printf("Tx broadcasted: %s\n", txHash)

    // 8. 获取合约 ID
    contractID, _ := createTx.ContractID()
    fmt.Printf("Contract deployed: %s\n", contractID)
    
    // 保存合约 ID 供后续调用使用
    fmt.Println("\nSave this Contract ID for Day 8!")
}
知识检查
  1. FATE VM 的 vmVersion 和 abiVersion 分别是多少?
  2. 如何为无参数的 init 函数编码 calldata?
  3. 合约 ID 的前缀是什么?
  4. 为什么合约部署需要比普通交易更多的 gas?