面向 AElf 的异步 Rust SDK。仓库采用 workspace 结构,将 client、crypto、keystore、proto、contract wrapper 和 facade crate 分层实现。
aelf-sdk.rust 的目标是对齐官方 C#、Go、Python SDK 的 core client 能力,并补齐 aelf-web3.js 常用的 wallet、JS-compatible keystore、contract_at 动态合约调用能力。
当前 v0.1 alpha 范围:
aelf-client提供 block、chain、network、transaction、utils 的异步 HTTP 服务aelf-crypto支持随机钱包、私钥导入、助记词导入、交易签名aelf-keystore提供与 JS SDK 兼容的 keystore 导入导出aelf-contract提供 Zero、Token、AEDPoS、CrossChain、Election、Vote 的 typed wrappercontract_at基于prost-reflect和链上 descriptor 实现动态合约调用- proto pipeline 基于 vendored proto +
prost-build + pbjson-build
当前 v0.1 不包含:
- Browser
wasm32-unknown-unknownruntime 支持 - Rust-only keystore 格式
- 类似 Python
toolkits.py的业务工具箱
| 能力 | 状态 | 说明 |
|---|---|---|
| Chain / block / net / tx client | 已实现 | 位于 aelf-client |
| Wallet / mnemonic / transaction signing | 已实现 | 默认 BIP44 路径 m/44'/1616'/0'/0/0 |
| JS-compatible keystore | 已实现 | 同时兼容 dklen / dkLen |
| Typed system contracts | 已实现 | Zero、Token、Election、Vote、CrossChain、AEDPoS |
| Dynamic contract calls | 已实现 | call_typed、call_json、send_typed、send_json |
| Proto vendoring pipeline | 已实现 | scripts/sync_proto.sh |
| 本地节点集成测试脚手架 | 已实现 | 默认 ignored,需要手动启用 |
wasm32-wasip2 自定义 provider 支持 |
已实现 | 使用 default-features = false + AElfClient::with_provider(...) |
Browser wasm32-unknown-unknown 支持 |
规划中 | v1 之后处理 |
已发布版本建议直接走 crates.io;如果需要使用当前 workspace 的最新代码,再走 path 依赖。
当前 crates.io 上已经发布的版本是 0.1.0-alpha.0。这条开发线里的 0.1.0-alpha.1 包含新的 wasm32-wasip2 custom-provider 能力,在正式发布前请通过 path 依赖接入。
[dependencies]
aelf-sdk = "0.1.0-alpha.0"
tokio = { version = "1", features = ["macros", "rt"] }Path 依赖:
[dependencies]
aelf-sdk = { path = "crates/aelf-sdk" }
tokio = { version = "1", features = ["macros", "rt"] }use aelf_sdk::{AElfClient, ClientConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = AElfClient::new(ClientConfig::new("http://127.0.0.1:8000"))?;
let status = client.chain().get_chain_status().await?;
println!(
"chain_id={} best_height={} genesis={}",
status.chain_id,
status.best_chain_height,
status.genesis_contract_address
);
Ok(())
}use aelf_sdk::{Keystore, Wallet};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let wallet = Wallet::create()?;
let keystore = Keystore::encrypt_js(&wallet, "123123")?;
let unlocked = keystore.unlock_js("123123")?;
assert_eq!(wallet.address(), unlocked.address);
assert_eq!(wallet.private_key(), unlocked.private_key);
assert_eq!(wallet.mnemonic(), unlocked.mnemonic);
Ok(())
}下面示例里的私钥是公开的只读测试 key,绝对不要充值或承载资产。
use aelf_sdk::proto::token::TransferInput;
use aelf_sdk::{AElfClient, ClientConfig, Wallet, decode_address};
use prost::Message;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = AElfClient::new(ClientConfig::new("http://127.0.0.1:8000"))?;
// 公开的只读测试 key,绝对不要充值或承载资产。
let wallet = Wallet::from_private_key(
"0000000000000000000000000000000000000000000000000000000000000001",
)?;
let tx = client
.transaction_builder()
.with_wallet(wallet)
.with_contract("TOKEN_CONTRACT_ADDRESS")
.with_method("Transfer")
.with_message(&TransferInput {
to: Some(aelf_sdk::proto::aelf::Address {
value: decode_address("ELF_2J...")?,
}),
symbol: "ELF".to_owned(),
amount: 1_0000_0000,
memo: "transfer from rust sdk".to_owned(),
})
.build_signed()
.await?;
let raw_transaction = hex::encode(tx.encode_to_vec());
let result = client.tx().send_transaction(&raw_transaction).await?;
println!("{}", result.transaction_id);
Ok(())
}公网节点补充说明:
send_transaction接收的是“已签名 protobuf bytes 的 hex 字符串”。/api/blockChain/rawTransaction的Params需要传 protobuf JSON 字符串,不能传 hex 编码的 protobuf bytes。execute_raw_transaction和send_raw_transaction的签名必须基于节点返回的 raw transaction bytes 计算。
下面示例里的私钥是公开的只读测试 key,绝对不要充值或承载资产。
use aelf_sdk::proto::token::GetBalanceInput;
use aelf_sdk::{AElfClient, ClientConfig, Wallet, address_to_pb};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = AElfClient::new(ClientConfig::new("http://127.0.0.1:8000"))?;
// 公开的只读测试 key,绝对不要充值或承载资产。
let wallet = Wallet::from_private_key(
"0000000000000000000000000000000000000000000000000000000000000001",
)?;
let token = client.token_contract("TOKEN_CONTRACT_ADDRESS", wallet);
let balance = token
.get_balance(&GetBalanceInput {
symbol: "ELF".to_owned(),
owner: Some(address_to_pb("ELF_2J...")?),
})
.await?;
println!("{}", balance.balance);
Ok(())
}下面示例里的私钥是公开的只读测试 key,绝对不要充值或承载资产。
use aelf_sdk::{AElfClient, ClientConfig, Wallet};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = AElfClient::new(ClientConfig::new("http://127.0.0.1:8000"))?;
// 公开的只读测试 key,绝对不要充值或承载资产。
let wallet = Wallet::from_private_key(
"0000000000000000000000000000000000000000000000000000000000000001",
)?;
let contract = client.contract_at("TOKEN_CONTRACT_ADDRESS", wallet).await?;
let balance = contract
.call_json(
"GetBalance",
serde_json::json!({
"symbol": "ELF",
"owner": "ELF_2J..."
}),
)
.await?;
println!("{balance}");
Ok(())
}真正的示例源码现在只保留在 crates/aelf-sdk/examples。根目录 /examples 只做薄包装转发,避免再维护两套实现。
cargo run -p aelf-sdk --example basic_client
cargo run -p aelf-sdk --example wallet_keystore_roundtrip
cargo run -p aelf-sdk --example public_balance
cargo run -p aelf-sdk --example token_transfer
cargo run -p aelf-sdk --example dynamic_contract_get_balance
cargo run -p aelf-sdk --example raw_transaction_flow常用环境变量:
AELF_ENDPOINTAELF_PRIVATE_KEYAELF_TOKEN_CONTRACTAELF_TO_ADDRESSAELF_OWNER_ADDRESSAELF_AMOUNTAELF_SEND
如果没有提供 AELF_PRIVATE_KEY,public_balance 和 dynamic_contract_get_balance 会回退到公开的只读测试 key。这个 key 仅用于示例和 smoke test,绝对不要充值或承载资产。
v0.1 alpha 当前有一个传输层 feature:
native-http(默认开启):启用HttpProvider和AElfClient::new(...)default-features = false:保留 transport-agnostic 形态,由 host runtime 自己实现Provider
在关闭默认 feature 后,wallet、keystore、transaction builder、typed contract、dynamic contract 仍然可用,只是 client 需要通过 AElfClient::with_provider(...) 构造。
aelf-sdk 现在可以被 Portkey 这一类 wasm32-wasip2 native-wasm skill runtime 消费。
WASM consumer 推荐关闭 native HTTP:
[dependencies]
aelf-sdk = { version = "0.1.0-alpha.1", default-features = false }
async-trait = "0.1"
http = "1"
serde_json = "1"然后由 host runtime 自己实现 Provider,再通过 with_provider(...) 构造 client:
use aelf_sdk::{AElfClient, AElfError, Provider};
use async_trait::async_trait;
use http::Method;
use serde_json::Value;
#[derive(Clone)]
struct HostProvider;
#[async_trait]
impl Provider for HostProvider {
async fn request_json(
&self,
_method: Method,
_path: &str,
_query: &[(&str, String)],
_body: Option<Value>,
) -> Result<Value, AElfError> {
Err(AElfError::request("host transport not wired", None))
}
async fn request_text(
&self,
_method: Method,
_path: &str,
_query: &[(&str, String)],
_body: Option<Value>,
) -> Result<String, AElfError> {
Err(AElfError::request("host transport not wired", None))
}
}
let client = AElfClient::with_provider(HostProvider)?;
# let _ = client;补充说明:
- SDK 不接管 Portkey / IronClaw 的
walletExport或 workspace memory 契约。 - Host runtime 继续维护自己的 HTTP binding 和 wallet store,
aelf-sdk负责链协议、签名、合约、交易等通用能力。 - Browser
wasm32-unknown-unknown仍然不在当前 alpha 范围内。
HttpProvider 默认会对瞬时失败进行自动重试:
RetryPolicy::default()=2次重试,初始退避200ms- 仅对
5xx响应和传输层临时错误重试 4xx会立即返回,不进入重试
你可以自定义或关闭重试:
use aelf_sdk::{AElfClient, ClientConfig, RetryPolicy};
use std::time::Duration;
let client = AElfClient::new(
ClientConfig::new("https://aelf-public-node.aelf.io")
.with_retry_policy(RetryPolicy::new(4, Duration::from_millis(100))),
)?;
let no_retry = AElfClient::new(
ClientConfig::new("https://aelf-public-node.aelf.io")
.without_retries(),
)?;
# let _ = (client, no_retry);send_transaction 现在只把 DTO 或 transaction-id 形态的字符串视为成功,像 "ok" 或代理错误文本这类非空文本会被拒绝为 unexpected response。
client.contract_at(...) 在每次新建 dynamic handle 时仍然会 fresh 拉一次 descriptor。typed contract wrapper 现在会在每个 handle 实例内 lazy 缓存第一次 descriptor,并在后续调用和 clone 后复用,但不会重新引入进程级全局 ABI cache。
编译整个 workspace:
cargo check --workspace
cargo check --workspace --examples
cargo check -p aelf-client --target wasm32-wasip2 --no-default-features
cargo check -p aelf-contract --target wasm32-wasip2 --no-default-features
cargo check -p aelf-sdk --target wasm32-wasip2 --no-default-features运行单元测试:
cargo test --workspace默认的 workspace test 故意不包含 ignored 的公网 live smoke,这样本地和 CI 的主测试集仍然保持确定性。
运行公网 readonly smoke:
cargo test -p aelf-sdk --test public_readonly_smoke -- --ignored --test-threads=1 --nocapture运行本地节点集成测试:
cargo test -p aelf-sdk --test local_node -- --ignoredignored 测试依赖以下环境变量:
AELF_ENDPOINTAELF_PRIVATE_KEYAELF_TOKEN_CONTRACTAELF_TO_ADDRESS
手动 funded transaction smoke 已放到 .github/workflows/transaction-smoke.yml,依赖以下仓库 secrets:
AELF_TRANSACTION_SMOKE_ENDPOINTAELF_TRANSACTION_SMOKE_PRIVATE_KEYAELF_TRANSACTION_SMOKE_TO_ADDRESSAELF_TRANSACTION_SMOKE_TOKEN_CONTRACT(可选)AELF_TRANSACTION_SMOKE_AMOUNT(可选)
已在 2026 年 3 月 10 日验证以下公网节点:
- 主链:https://aelf-public-node.aelf.io/swagger/index.html
- 侧链:https://tdvv-public-node.aelf.io/swagger/index.html
已验证链路:
basic_client在主链和侧链都通过- readonly block / chain / net / tx pool 集成测试在主链和侧链都通过
- Zero -> Token typed contract 调用在主链和侧链都通过
contract_at(...).call_json("GetBalance", ...)在主链和侧链都通过send_transaction已在主链通过create_raw_transaction -> execute_raw_transaction -> send_raw_transaction -> get_transaction_result已在主链通过
观察到的兼容性结论:
- dynamic contract 的 JSON 输入输出层会把
aelf.Address/aelf.Hash在开发者友好的字符串和 protobuf JSON object 之间做双向归一化。 create_raw_transaction只有在Params传 protobuf JSON 时才会成功;如果传 hex 编码的 protobuf bytes,主链公网节点会返回403 Invalid params。execute_raw_transaction和send_raw_transaction的签名必须基于节点生成的 raw transaction bytes,而不是本地构造的Transaction对象直接复用签名。calculate_transaction_fee在公网节点上可能返回空 map,即使交易最终已经被接受并成功出块。
常用命令:
AELF_ENDPOINT='https://aelf-public-node.aelf.io' cargo run -p aelf-sdk --example basic_client
AELF_ENDPOINT='https://aelf-public-node.aelf.io' AELF_OWNER_ADDRESS='<address>' cargo run -p aelf-sdk --example public_balance
AELF_ENDPOINT='https://aelf-public-node.aelf.io' AELF_PRIVATE_KEY='<private-key>' AELF_TO_ADDRESS='ELF_<address>_AELF' AELF_AMOUNT='1' cargo run -p aelf-sdk --example token_transfer
AELF_ENDPOINT='https://aelf-public-node.aelf.io' AELF_PRIVATE_KEY='<private-key>' AELF_TO_ADDRESS='ELF_<address>_AELF' AELF_AMOUNT='1' cargo run -p aelf-sdk --example raw_transaction_flow仓库结构:
crates/
aelf-sdk
aelf-client
aelf-contract
aelf-crypto
aelf-keystore
aelf-proto
examples/
proto/upstream/
scripts/sync_proto.sh
tests/fixtures/
发布流程:
- 用
scripts/sync_proto.sh同步 upstream proto - 执行
cargo fmt、cargo +1.85.0 check --workspace --all-targets --all-features --locked、cargo clippy --workspace --all-targets --all-features、cargo audit、cargo check --workspace --examples、cargo test --workspace - 检查
CHANGELOG.md,然后先手动运行一次publishGitHub Actions workflow,并把dry_run设为true。全量发版时保持packages为空;如果只是验证恢复路径,可设置packages=aelf-sdk - 确认 crates.io token 已配置后,再以
dry_run=false重新运行publishworkflow。保留skip_published=true,这样在部分发布成功后可以安全重试 - 如果 crates.io 在部分 crate 已发布后返回瞬时错误,重新运行 workflow,并设置
dry_run=false、packages=<剩余-crates>、skip_published=true。针对 2026 年 3 月 10 日这次事故,恢复时应使用packages=aelf-sdk
发布说明:
- 发布 workflow 会按依赖顺序发布:
aelf-proto、aelf-crypto、aelf-client、aelf-keystore、aelf-contract、aelf-sdk - 全量 dry-run 仍然使用
cargo publish --workspace --dry-run --locked,这样可以一起验证尚未发布但彼此依赖的 workspace 版本 - crates.io 已发布版本不可覆盖;如果某个版本发布内容有误,只能先
yank,再发布新版本
CI 定义在 .github/workflows/ci.yml。
发布流程定义在 .github/workflows/publish.yml,需要配置仓库 secret CARGO_REGISTRY_TOKEN。
MSRV 说明:
- workspace 的 MSRV 现在是 Rust
1.85。 - CI 已用
cargo +1.85.0 check --workspace --all-targets --all-features --locked做硬性门禁。
私下披露漏洞的方式见 SECURITY.md。
MIT