Skip to content

Latest commit

 

History

History
450 lines (338 loc) · 15.9 KB

File metadata and controls

450 lines (338 loc) · 15.9 KB

aelf-sdk.rust

面向 AElf 的异步 Rust SDK。仓库采用 workspace 结构,将 client、crypto、keystore、proto、contract wrapper 和 facade crate 分层实现。

Overview

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 wrapper
  • contract_at 基于 prost-reflect 和链上 descriptor 实现动态合约调用
  • proto pipeline 基于 vendored proto + prost-build + pbjson-build

当前 v0.1 不包含:

  • Browser wasm32-unknown-unknown runtime 支持
  • Rust-only keystore 格式
  • 类似 Python toolkits.py 的业务工具箱

Feature Matrix

能力 状态 说明
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_typedcall_jsonsend_typedsend_json
Proto vendoring pipeline 已实现 scripts/sync_proto.sh
本地节点集成测试脚手架 已实现 默认 ignored,需要手动启用
wasm32-wasip2 自定义 provider 支持 已实现 使用 default-features = false + AElfClient::with_provider(...)
Browser wasm32-unknown-unknown 支持 规划中 v1 之后处理

Install

已发布版本建议直接走 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"] }

Quick Start

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(())
}

Wallet & Keystore

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(())
}

Raw Transaction

下面示例里的私钥是公开的只读测试 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/rawTransactionParams 需要传 protobuf JSON 字符串,不能传 hex 编码的 protobuf bytes。
  • execute_raw_transactionsend_raw_transaction 的签名必须基于节点返回的 raw transaction bytes 计算。

Typed Contracts

下面示例里的私钥是公开的只读测试 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(())
}

Dynamic Contracts

下面示例里的私钥是公开的只读测试 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(())
}

Examples

真正的示例源码现在只保留在 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_ENDPOINT
  • AELF_PRIVATE_KEY
  • AELF_TOKEN_CONTRACT
  • AELF_TO_ADDRESS
  • AELF_OWNER_ADDRESS
  • AELF_AMOUNT
  • AELF_SEND

如果没有提供 AELF_PRIVATE_KEYpublic_balancedynamic_contract_get_balance 会回退到公开的只读测试 key。这个 key 仅用于示例和 smoke test,绝对不要充值或承载资产。

Feature Flags

v0.1 alpha 当前有一个传输层 feature:

  • native-http(默认开启):启用 HttpProviderAElfClient::new(...)
  • default-features = false:保留 transport-agnostic 形态,由 host runtime 自己实现 Provider

在关闭默认 feature 后,wallet、keystore、transaction builder、typed contract、dynamic contract 仍然可用,只是 client 需要通过 AElfClient::with_provider(...) 构造。

Native WASM (wasm32-wasip2)

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 范围内。

Transport Behavior

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。

Local Node Testing

编译整个 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 -- --ignored

ignored 测试依赖以下环境变量:

  • AELF_ENDPOINT
  • AELF_PRIVATE_KEY
  • AELF_TOKEN_CONTRACT
  • AELF_TO_ADDRESS

手动 funded transaction smoke 已放到 .github/workflows/transaction-smoke.yml,依赖以下仓库 secrets:

  • AELF_TRANSACTION_SMOKE_ENDPOINT
  • AELF_TRANSACTION_SMOKE_PRIVATE_KEY
  • AELF_TRANSACTION_SMOKE_TO_ADDRESS
  • AELF_TRANSACTION_SMOKE_TOKEN_CONTRACT(可选)
  • AELF_TRANSACTION_SMOKE_AMOUNT(可选)

Public Node Verification

已在 2026 年 3 月 10 日验证以下公网节点:

已验证链路:

  • 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_transactionsend_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

Release

仓库结构:

crates/
  aelf-sdk
  aelf-client
  aelf-contract
  aelf-crypto
  aelf-keystore
  aelf-proto
examples/
proto/upstream/
scripts/sync_proto.sh
tests/fixtures/

发布流程:

  1. scripts/sync_proto.sh 同步 upstream proto
  2. 执行 cargo fmtcargo +1.85.0 check --workspace --all-targets --all-features --lockedcargo clippy --workspace --all-targets --all-featurescargo auditcargo check --workspace --examplescargo test --workspace
  3. 检查 CHANGELOG.md,然后先手动运行一次 publish GitHub Actions workflow,并把 dry_run 设为 true。全量发版时保持 packages 为空;如果只是验证恢复路径,可设置 packages=aelf-sdk
  4. 确认 crates.io token 已配置后,再以 dry_run=false 重新运行 publish workflow。保留 skip_published=true,这样在部分发布成功后可以安全重试
  5. 如果 crates.io 在部分 crate 已发布后返回瞬时错误,重新运行 workflow,并设置 dry_run=falsepackages=<剩余-crates>skip_published=true。针对 2026 年 3 月 10 日这次事故,恢复时应使用 packages=aelf-sdk

发布说明:

  • 发布 workflow 会按依赖顺序发布:aelf-protoaelf-cryptoaelf-clientaelf-keystoreaelf-contractaelf-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

License

MIT