Day 5 合约部署与调用

掌握合约的完整生命周期和调用机制。

学习目标
  • 掌握合约的完整生命周期
  • 理解合约调用机制
  • 熟悉 Dry-Run 模拟执行
  • 使用 ContractManager 管理合约

1. 合约部署流程

┌─────────────────────────────────────────────────────────────┐
│                    合约部署流程                              │
├─────────────────────────────────────────────────────────────┤
│   1. 编写 Sophia 源码                                       │
│        │                                                    │
│        ▼                                                    │
│   2. 编译获取字节码 (cb_...)                                │
│        │                                                    │
│        ▼                                                    │
│   3. 生成 init() 的 calldata                                │
│        │                                                    │
│        ▼                                                    │
│   4. 构建 ContractCreateTx                                  │
│        │                                                    │
│        ▼                                                    │
│   5. 签名并广播交易                                          │
│        │                                                    │
│        ▼                                                    │
│   6. 获取合约地址 (ct_...)                                  │
└─────────────────────────────────────────────────────────────┘

2. 调用类型

方式上链消耗 Gas修改状态返回值
call交易哈希
call_static直接结果
dry_run模拟模拟模拟结果
需要修改状态?
    │
    ├── 是 → 使用 call(需要付费)
    │
    └── 否 → 只读查询?
              │
              ├── 是 → 使用 call_static(免费)
              │
              └── 否 → 测试/调试 → 使用 dry_run

3. 实践任务

任务 5.1: 使用 ContractManager 部署合约
from aeternity.node import NodeClient, Config
from aeternity.signing import Account
from aeternity.contract_native import ContractManager

# 准备
client = NodeClient(Config(external_url='https://testnet.aeternity.io'))

# 合约源码
source = '''
@compiler >= 6

contract Counter =
    record state = { count : int }
    
    entrypoint init(start : int) = { count = start }
    
    entrypoint get() : int = state.count
    
    stateful entrypoint increment() =
        put(state{ count = state.count + 1 })
    
    stateful entrypoint add(n : int) =
        put(state{ count = state.count + n })
'''

# 部署流程(需要有余额的账户)
"""
# 1. 创建 ContractManager
manager = ContractManager(
    client=client,
    account=account,
    source=source
)

# 2. 部署合约(调用 init 函数)
tx = manager.deploy("0")  # init(0)

# 3. 获取合约地址
print(f"合约地址: {manager.contract_id}")
print(f"部署交易: {tx.hash}")

# 4. 调用合约
# 写入操作(上链)
tx = manager.call("increment")
tx = manager.call("add", "10")

# 读取操作(不上链)
result = manager.call_static("get")
print(f"当前值: {result}")
"""
任务 5.2: 低级 API 部署
from aeternity.node import NodeClient, Config
from aeternity.signing import Account
from aeternity.compiler import CompilerClient
from aeternity import hashing

client = NodeClient(Config(external_url='https://testnet.aeternity.io'))
compiler = CompilerClient()

source = '''
@compiler >= 6

contract SimpleStorage =
    record state = { value : int }
    entrypoint init(v : int) = { value = v }
    entrypoint get() : int = state.value
    stateful entrypoint set(v : int) = put(state{ value = v })
'''

print("=== 低级 API 部署流程 ===\n")

# 1. 编译合约
print("1. 编译合约...")
bytecode = compiler.compile(source)
print(f"   字节码: {bytecode[:40]}...")

# 2. 生成 init calldata
print("\n2. 生成 init calldata...")
init_calldata = compiler.encode_calldata(source, "init", ["42"])
print(f"   Calldata: {init_calldata[:40]}...")

# 3. 准备部署参数
print("\n3. 部署参数:")
print("""
   owner_id: 部署者地址
   bytecode: 编译后的字节码
   calldata: init 函数的参数
   amount: 0(除非 init 是 payable)
   gas: 100000(根据合约复杂度调整)
   gas_price: 1000000000
""")

# 4. 构建部署交易(伪代码)
print("\n4. 构建部署交易:")
print("""
# 获取 nonce 和 VM 版本
nonce = client.get_next_nonce(account.get_address())
vm_version, abi_version = client.get_vm_abi_versions()

# 构建交易
tx = client.tx_builder.tx_contract_create(
    owner_id=account.get_address(),
    code=bytecode,
    calldata=init_calldata,
    amount=0,
    gas=100000,
    gas_price=1000000000,
    vm_version=vm_version,
    abi_version=abi_version,
    nonce=nonce
)

# 计算合约地址
contract_id = hashing.contract_id(account.get_address(), nonce)
print(f"预计合约地址: {contract_id}")
""")
任务 5.3: 处理合约事件
@compiler >= 6

contract EventDemo =
    datatype event = 
        Transfer(address, address, int)
        | Approval(address, address, int)
    
    record state = { 
        balances : map(address, int),
        allowances : map((address, address), int)
    }
    
    entrypoint init() = {
        balances = {},
        allowances = {}
    }
    
    stateful entrypoint transfer(to : address, amount : int) =
        let from = Call.caller
        // ... 转账逻辑 ...
        Chain.event(Transfer(from, to, amount))
    
    stateful entrypoint approve(spender : address, amount : int) =
        let owner = Call.caller
        // ... 授权逻辑 ...
        Chain.event(Approval(owner, spender, amount))
# 读取合约事件

# 获取交易信息
tx_info = client.get_transaction_info_by_hash(hash=tx_hash)

# 解析事件
for log in tx_info.call_info.log:
    event_hash = log.topics[0]  # 事件类型哈希
    event_data = log.data       # 事件数据
    
# 使用 ContractManager 解码
events = manager.decode_events(tx_hash)
for event in events:
    print(f"{event.name}: {event.args}")
任务 5.4: Dry-Run 模拟

Dry-Run 允许在不上链的情况下模拟交易执行:

  • 不消耗真实 Gas
  • 不修改链上状态
  • 可以预估 Gas 消耗
  • 可以获取返回值
# 使用 Dry-Run
result = contract.call_static(
    contract_id="ct_xxx...",
    function="add",
    calldata=compiler.encode_calldata(source, "add", ["10", "20"])
)

# 结果包含
print(f"返回值: {result.return_value}")
print(f"Gas 消耗: {result.gas_used}")
print(f"调用结果: {result.call_type}")  # ok 或 revert

4. 常见问题

# 增加 gas 限制
manager = ContractManager(
    client=client,
    account=account,
    source=source,
    gas=500000  # 增加 gas
)

  • 确认合约已成功部署
  • 检查合约地址是否正确
  • 确认在正确的网络上

  • 检查合约的 require 条件
  • 确认调用参数正确
  • 使用 dry-run 调试
知识检查点
  • 使用 ContractManager 部署合约
  • 区分 call 和 call_static
  • 使用 Dry-Run 测试
  • 读取合约事件
  • 处理合约错误