核心
智能合约开发
完整的 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 实例时需要提供账户,用于签名 ContractCreateTx 和 ContractCallTx 等交易。
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 |
42n 或 undefined |
record |
Object |
{ name: "Alice", age: 30n } |
variant |
Object |
{ Some: [42n] } |
继续学习