高级
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:3080 | Docker 部署本地编译器 |
初始化编译器连接
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_..." |
bool | true/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. 创建部署交易
部署参数说明
| 参数 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
| vmVersion | uint16 | 虚拟机版本 | 5 (FATE) |
| abiVersion | uint16 | ABI 版本 | 3 (FATE) |
| deposit | *big.Int | 押金(通常为0) | 0 |
| amount | *big.Int | 转给合约的金额 | 0 或所需金额 |
| gasLimit | *big.Int | 最大 Gas 消耗 | 100000+ |
| gasPrice | *big.Int | Gas 单价 | 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!")
}
知识检查
- FATE VM 的 vmVersion 和 abiVersion 分别是多少?
- 如何为无参数的 init 函数编码 calldata?
- 合约 ID 的前缀是什么?
- 为什么合约部署需要比普通交易更多的 gas?
本页目录