核心

智能合约开发

完整的 Sophia 合约开发流程:编译、部署、调用

概述

Æternity 区块链的智能合约语言是 Sophia。它是 ML 家族的函数式语言,强类型且限制可变状态。

在使用 SDK 与合约交互之前,建议先熟悉 Sophia 语言本身。可以查看 aepp-sophia-examples 并使用 AEstudio 进行快速原型开发。

步骤 1 导入依赖
// Node.js 导入
const { AeSdk, AccountMemory, Node } = require('@aeternity/aepp-sdk');

// ES6 导入
import { AeSdk, AccountMemory, Node } from '@aeternity/aepp-sdk';

// 如果需要编译合约,还需导入编译器
import { CompilerCli, CompilerHttp } from '@aeternity/aepp-sdk';
步骤 2 配置编译器

编译器主要用于生成部署合约所需的字节码。如果你已有合约字节码或只需与已部署的合约交互,可跳过此步骤。

SDK 开箱即用地支持两种编译器:

CompilerCli

基于本地命令行编译器

  • 仅在 Node.js 环境可用
  • 需要安装 Erlang(escript 在 PATH 中)
  • 支持 Windows
const compiler = new CompilerCli();
CompilerHttp

基于 HTTP 服务的编译器

  • 需要托管编译器服务
  • 建议自行部署编译服务
  • 可在浏览器中使用
const compiler = new CompilerHttp('https://v8.compiler.aepps.com');
注意:公共编译器服务 compiler.aepps.com 计划停用,建议自行托管编译服务。
步骤 3 创建 SDK 实例

创建 SDK 实例时需要提供账户,用于签名 ContractCreateTxContractCallTx 等交易。

const node = new Node('https://testnet.aeternity.io'); // 建议自行托管节点
const account = new AccountMemory(SECRET_KEY);

const aeSdk = new AeSdk({
  nodes: [{ name: 'testnet', instance: node }],
  accounts: [account],
  onCompiler: compiler, // 如跳过步骤 2 则移除此行
});
提示:
  • 可以向 SDK 提供多个账户
  • 每笔交易可选择特定账户签名(默认使用第一个账户)
  • 这在编写测试时特别有用
步骤 4 初始化合约实例

初始化合约实例有多种方式:

通过源代码
const sourceCode = `
contract Increment =
    record state = { count: int }
    entrypoint init(start: int) = { count = start }
    stateful entrypoint increment(value: int) =
        put(state{ count = state.count + value })
    entrypoint get_count() = state.count
`;

const options = { sourceCode };

// 如果合约包含外部依赖
const fileSystem = {
  'library.aes': '... 库源代码 ...'
};
const optionsWithDeps = { sourceCode, fileSystem };
通过文件路径(仅 Node.js)
// 自动处理合约导入,无需提供 fileSystem
const sourceCodePath = './example.aes';
const options = { sourceCodePath };
通过 ACI 和字节码
// 适用于预编译的合约
const aci = { ... }; // 合约 ACI
const bytecode = 'cb_...'; // 合约字节码
const options = { aci, bytecode };
通过 ACI 和合约地址
// 与已部署合约交互,无需编译器
const aci = { ... }; // 合约 ACI
const address = 'ct_...'; // 已部署的合约地址
const options = { aci, address };
创建合约实例
import { Contract } from '@aeternity/aepp-sdk';

const contract = await Contract.initialize({ 
  ...aeSdk.getContext(), 
  ...options 
});

AeSdk.getContext() 获取当前账户、节点和编译器等基础配置。如果你更改了 AeSdk 中的节点,绑定的合约实例也会自动更新。

步骤 5 部署合约

假设你有如下 Sophia 合约:

contract Increment =
    record state = { count: int }
    
    entrypoint init(start: int) = { count = start }
    
    stateful entrypoint increment(value: int) =
        put(state{ count = state.count + value })
    
    entrypoint get_count() = state.count

部署合约:

// 方式一(推荐)
const tx = await contract.$deploy([1]); // 初始值为 1

// 方式二
const tx = await contract.init(1);

// 部署成功后可查看交易信息
console.log(tx);
/*
{
  owner: 'ak_...',
  transaction: 'th_...',
  address: 'ct_...',  // 合约地址
  result: { ... },
  rawTx: 'tx_...'
}
*/
注意:
  • init 入口点是特殊函数,仅在部署时调用一次,用于初始化合约状态
  • init 不需要声明为 stateful
  • 只有提供源代码或字节码时才能部署
步骤 6 调用合约入口点
a) 有状态入口点 (Stateful)

调用会修改链上状态的函数,需要签名并广播交易:

// 推荐方式
const tx = await contract.increment(3);

// 或显式指定 callStatic: false
const tx = await contract.increment(3, { callStatic: false });

// 或使用 $call
const tx = await contract.$call('increment', [3]);
b) 只读入口点

调用不修改状态的函数,使用 dry-run 获取结果,不广播交易:

// 推荐方式
const tx = await contract.get_count();

// 或显式指定 callStatic: true
const tx = await contract.get_count({ callStatic: true });

// 访问解码后的返回值
console.log(tx.decodedResult); // 输出:4
c) 可支付入口点 (Payable)

调用需要转入 AE 的函数:

// Sophia 代码
payable stateful entrypoint fund_project(project_id: int) =
    require(Call.value >= 50, "需要至少 50 aettos")
    // 后续逻辑...
// JavaScript 调用
const tx = await contract.fund_project(1, { amount: 50 });

// 或使用 $call
const tx = await contract.$call('fund_project', [1], { amount: 50 });
Sophia 数据类型映射

JavaScript 和 Sophia 之间的类型转换由 aepp-calldata 库处理。常见映射:

Sophia 类型 JavaScript 类型 示例
int bigint 42n
bool boolean true
string string "hello"
address string "ak_..."
list(T) Array [1n, 2n, 3n]
map(K, V) Map new Map([["a", 1n]])
option(T) T | undefined 42nundefined
record Object { name: "Alice", age: 30n }
variant Object { Some: [42n] }