diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4bdf2b..f0d00ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,27 @@ jobs: - name: msrv run: cargo check --workspace --all-targets --all-features --locked - test: + wasm: runs-on: ubuntu-latest needs: msrv + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip2 + - uses: Swatinem/rust-cache@v2 + - name: wasm aelf-client + run: cargo check -p aelf-client --target wasm32-wasip2 --no-default-features + - name: wasm aelf-contract + run: cargo check -p aelf-contract --target wasm32-wasip2 --no-default-features + - name: wasm aelf-sdk + run: cargo check -p aelf-sdk --target wasm32-wasip2 --no-default-features + + test: + runs-on: ubuntu-latest + needs: + - msrv + - wasm steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -35,3 +53,28 @@ jobs: run: cargo check --workspace --examples - name: test run: cargo test --workspace --all-targets + + public-smoke: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: public readonly smoke + run: | + retry() { + local attempts=3 + local delay=5 + for attempt in $(seq 1 "$attempts"); do + if "$@"; then + return 0 + fi + if [[ "$attempt" -lt "$attempts" ]]; then + sleep "$delay" + fi + done + return 1 + } + + retry cargo test -p aelf-sdk --test public_readonly_smoke -- --ignored --test-threads=1 --nocapture diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fd5f881..4124781 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -147,17 +147,17 @@ jobs: return 1 } + if [[ "$DRY_RUN" != "true" && -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then + echo "::error::CARGO_REGISTRY_TOKEN is required when dry_run=false." + exit 1 + fi + if [[ "$DRY_RUN" == "true" && "${#requested_packages[@]}" -eq 0 ]]; then echo "Running workspace dry-run for the full publish set." cargo publish --workspace --dry-run --locked exit 0 fi - if [[ "$DRY_RUN" != "true" && -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then - echo "::error::CARGO_REGISTRY_TOKEN is required when dry_run=false." - exit 1 - fi - for pkg in "${ordered_packages[@]}"; do if ! package_selected "$pkg"; then continue diff --git a/.github/workflows/transaction-smoke.yml b/.github/workflows/transaction-smoke.yml new file mode 100644 index 0000000..380eb6d --- /dev/null +++ b/.github/workflows/transaction-smoke.yml @@ -0,0 +1,23 @@ +name: transaction-smoke + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + funded-transaction-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: funded transaction smoke + env: + AELF_ENDPOINT: ${{ secrets.AELF_TRANSACTION_SMOKE_ENDPOINT }} + AELF_PRIVATE_KEY: ${{ secrets.AELF_TRANSACTION_SMOKE_PRIVATE_KEY }} + AELF_TO_ADDRESS: ${{ secrets.AELF_TRANSACTION_SMOKE_TO_ADDRESS }} + AELF_TOKEN_CONTRACT: ${{ secrets.AELF_TRANSACTION_SMOKE_TOKEN_CONTRACT }} + AELF_AMOUNT: ${{ secrets.AELF_TRANSACTION_SMOKE_AMOUNT }} + run: cargo test -p aelf-sdk --test funded_transaction_smoke funded_send_transaction_smoke -- --ignored --exact --nocapture diff --git a/CHANGELOG.md b/CHANGELOG.md index 8015b10..a7554a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to `aelf-sdk.rust` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.0-alpha.1] - 2026-03-11 + +### Added + +- `native-http` Cargo feature on `aelf-client`, `aelf-contract`, and `aelf-sdk`, enabled by default for native consumers +- `AElfClient::with_provider(...)` and `Provider` re-exports on the facade crate for custom transports +- `wasm32-wasip2` CI compile gates for `aelf-client`, `aelf-contract`, and `aelf-sdk` with `--no-default-features` +- public readonly smoke coverage in a dedicated CI job plus a manual funded transaction smoke workflow + +### Changed + +- The workspace version moved to `0.1.0-alpha.1` +- Tokio now uses the wasm-compatible `rt` feature instead of `rt-multi-thread` +- `HttpProvider` and `AElfClient::new(...)` are now gated behind `native-http`, while the core SDK remains provider-first +- root `/examples` now forward to `crates/aelf-sdk/examples` so the SDK only maintains one example source of truth +- wallet keystore examples now redact private key and mnemonic output + +### Fixed + +- `aelf-client`, `aelf-contract`, and `aelf-sdk` now compile for `wasm32-wasip2` when consumed with `default-features = false` +- The facade crate now exposes the transport abstraction needed by native-wasm skill runtimes +- `send_transaction` no longer treats arbitrary non-empty text payloads as success +- typed contract wrappers now lazily reuse the first descriptor per handle, while direct `contract_at(...)` calls still fetch a fresh descriptor for each new handle + ## [0.1.0-alpha.0] - 2026-03-10 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df965ef..9ab4954 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,15 +20,21 @@ cargo +1.85.0 check --workspace --all-targets --all-features --locked cargo clippy --workspace --all-targets --all-features -- -D warnings cargo audit 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 ``` -Optional local-node validation: +Optional live-node validation: ```bash cargo test -p aelf-sdk --test local_node -- --ignored +cargo test -p aelf-sdk --test public_readonly_smoke -- --ignored --test-threads=1 ``` +`cargo test --workspace` intentionally excludes the ignored live smoke suites so the default test pass remains deterministic and offline-friendly. + ## Repository Layout - `crates/aelf-sdk`: public facade crate @@ -37,7 +43,8 @@ cargo test -p aelf-sdk --test local_node -- --ignored - `crates/aelf-crypto`: wallet, signing, address utilities - `crates/aelf-keystore`: JS-compatible keystore support - `crates/aelf-proto`: generated protobuf bindings -- `examples/`: runnable examples wired into `aelf-sdk` +- `crates/aelf-sdk/examples`: canonical example sources +- `examples/`: thin forwarding wrappers for local convenience ## Pull Requests @@ -46,6 +53,7 @@ cargo test -p aelf-sdk --test local_node -- --ignored - Document public API additions with rustdoc. - Update `README.md`, `README.zh.md`, or `CHANGELOG.md` when user-facing behavior changes. - Keep the documented MSRV at Rust `1.85` and preserve the hard `cargo +1.85.0 check --workspace --all-targets --all-features --locked` CI gate. +- Preserve the `wasm32-wasip2` compile gates for `aelf-client`, `aelf-contract`, and `aelf-sdk` when changing transport or feature-flag behavior. ## Commit Style diff --git a/Cargo.lock b/Cargo.lock index 66c9a11..002e352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "aelf-client" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" dependencies = [ "aelf-crypto", "aelf-proto", @@ -25,14 +25,16 @@ dependencies = [ [[package]] name = "aelf-contract" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" dependencies = [ "aelf-client", "aelf-crypto", "aelf-proto", + "async-trait", + "base64 0.22.1", "bytes", "hex", - "lru", + "http", "pbjson-types", "prost", "prost-reflect", @@ -44,7 +46,7 @@ dependencies = [ [[package]] name = "aelf-crypto" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" dependencies = [ "aelf-proto", "bs58", @@ -60,7 +62,7 @@ dependencies = [ [[package]] name = "aelf-keystore" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" dependencies = [ "aelf-crypto", "aes", @@ -82,7 +84,7 @@ dependencies = [ [[package]] name = "aelf-proto" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" dependencies = [ "pbjson", "pbjson-build", @@ -96,7 +98,7 @@ dependencies = [ [[package]] name = "aelf-sdk" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" dependencies = [ "aelf-client", "aelf-contract", @@ -129,12 +131,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "anyhow" version = "1.0.102" @@ -641,12 +637,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -843,7 +833,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash 0.1.5", + "foldhash", ] [[package]] @@ -851,11 +841,6 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] [[package]] name = "heck" @@ -1247,15 +1232,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "lru-slab" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 051110d..661d891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ edition = "2021" license = "MIT" repository = "https://github.com/AElfProject/aelf-web3.rust" rust-version = "1.85" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" [workspace.dependencies] async-trait = "0.1.89" @@ -30,7 +30,6 @@ coins-bip39 = "0.13.0" ctr = "0.9.2" hex = "0.4.3" http = "1.3.1" -lru = "0.16.3" pbjson = "0.9.0" pbjson-build = "0.9.0" pbjson-types = "0.9.0" @@ -49,7 +48,7 @@ serde_json = "1.0.140" sha2 = "0.10.9" sha3 = "0.10.8" thiserror = "2.0.18" -tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "sync", "time"] } +tokio = { version = "1.47.1", features = ["macros", "rt", "sync", "time"] } zeroize = "1.8.1" [workspace.lints.rust] diff --git a/README.md b/README.md index 46e6335..61d71e6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Current v0.1 alpha scope: Out of scope for v0.1: -- WASM/browser runtime support +- Browser `wasm32-unknown-unknown` runtime support - Rust-only keystore format - Business toolkits similar to `toolkits.py` @@ -32,24 +32,27 @@ Out of scope for v0.1: | Dynamic contract calls | Implemented | `call_typed`, `call_json`, `send_typed`, `send_json` | | Proto vendoring pipeline | Implemented | `scripts/sync_proto.sh` | | Local node integration test scaffold | Implemented | Ignored by default, opt in with `-- --ignored` | -| WASM support | Planned | Post-v1 | +| `wasm32-wasip2` custom-provider support | Implemented | Use `default-features = false` + `AElfClient::with_provider(...)` | +| Browser `wasm32-unknown-unknown` support | Planned | Post-v1 | ## Install -Until the crate is published to crates.io, use a path or git dependency. +Use crates.io for published releases, or a path dependency while working against the workspace tip. + +The latest published release on crates.io is currently `0.1.0-alpha.0`. The `0.1.0-alpha.1` workspace tip includes the new `wasm32-wasip2` custom-provider flow and can be consumed via a path dependency until it is published. ```toml [dependencies] -aelf-sdk = { path = "crates/aelf-sdk" } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +aelf-sdk = "0.1.0-alpha.0" +tokio = { version = "1", features = ["macros", "rt"] } ``` -Git dependency: +Path dependency: ```toml [dependencies] -aelf-sdk = { git = "https://github.com/AElfProject/aelf-web3.rust", tag = "v0.1.0-alpha.0" } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +aelf-sdk = { path = "crates/aelf-sdk" } +tokio = { version = "1", features = ["macros", "rt"] } ``` ## Quick Start @@ -190,7 +193,7 @@ async fn main() -> Result<(), Box> { ## Examples -Examples are stored in `/examples` and wired into the `aelf-sdk` crate. +The canonical example sources live in `crates/aelf-sdk/examples`. The root `/examples` directory now contains thin forwarding wrappers so there is only one maintained implementation. ```bash cargo run -p aelf-sdk --example basic_client @@ -213,11 +216,70 @@ Useful environment variables: ## Feature Flags -v0.1 alpha intentionally keeps the surface small. There are no optional Cargo feature flags yet. +v0.1 alpha exposes one transport feature: + +- `native-http` (default): enables `HttpProvider` and `AElfClient::new(...)` +- `default-features = false`: keeps the SDK transport-agnostic so a host runtime can provide its own `Provider` + +Transport-independent builds still support wallet, keystore, transaction building, typed contracts, and dynamic contracts through `AElfClient::with_provider(...)`. + +## Native WASM (`wasm32-wasip2`) + +`aelf-sdk` can now be consumed from native-wasm skill runtimes such as Portkey-style `wasm32-wasip2` sidecars. -- Runtime: Tokio only -- TLS: `reqwest` with `rustls` -- Proto JSON: `pbjson` +Use the facade without native HTTP: + +```toml +[dependencies] +aelf-sdk = { version = "0.1.0-alpha.1", default-features = false } +async-trait = "0.1" +http = "1" +serde_json = "1" +``` + +Then implement `Provider` in the host runtime and build the client with `with_provider(...)`: + +```rust +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, + ) -> Result { + Err(AElfError::request("host transport not wired", None)) + } + + async fn request_text( + &self, + _method: Method, + _path: &str, + _query: &[(&str, String)], + _body: Option, + ) -> Result { + Err(AElfError::request("host transport not wired", None)) + } +} + +let client = AElfClient::with_provider(HostProvider)?; +# let _ = client; +``` + +Notes: + +- The SDK does not own Portkey / IronClaw `walletExport` or workspace-memory contracts. +- Host runtimes should keep their own HTTP bindings and wallet storage layers, and delegate chain protocol logic to `aelf-sdk`. +- Browser `wasm32-unknown-unknown` remains out of scope for this alpha line. ## Transport Behavior @@ -245,7 +307,9 @@ let no_retry = AElfClient::new( # let _ = (client, no_retry); ``` -Dynamic contract descriptors are cached in-memory using a `64`-entry LRU cache to avoid unbounded growth in long-running services. +`send_transaction` only treats DTO payloads or transaction-id-shaped strings as success. Non-empty text such as `"ok"` or proxy error bodies are rejected as unexpected responses. + +`client.contract_at(...)` still fetches a fresh descriptor whenever you create a new dynamic handle. Typed contract wrappers now lazily cache the first descriptor per handle instance and reuse it across subsequent calls and clones, without reintroducing a process-wide ABI cache. ## Local Node Testing @@ -254,6 +318,9 @@ Compile everything: ```bash 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 ``` Run unit tests: @@ -262,6 +329,14 @@ Run unit tests: cargo test --workspace ``` +The default workspace test pass intentionally excludes the ignored live public-node smoke suite so local and CI runs remain deterministic. + +Run the public readonly smoke suite: + +```bash +cargo test -p aelf-sdk --test public_readonly_smoke -- --ignored --test-threads=1 --nocapture +``` + Run ignored local-node integration tests: ```bash @@ -275,6 +350,14 @@ The ignored suite expects: - `AELF_TOKEN_CONTRACT` - `AELF_TO_ADDRESS` +Manual funded transaction smoke is available through `.github/workflows/transaction-smoke.yml` and expects these repository secrets: + +- `AELF_TRANSACTION_SMOKE_ENDPOINT` +- `AELF_TRANSACTION_SMOKE_PRIVATE_KEY` +- `AELF_TRANSACTION_SMOKE_TO_ADDRESS` +- `AELF_TRANSACTION_SMOKE_TOKEN_CONTRACT` (optional) +- `AELF_TRANSACTION_SMOKE_AMOUNT` (optional) + ## Public Node Verification Verified on March 10, 2026 against: diff --git a/README.zh.md b/README.zh.md index 36407cf..924a2e5 100644 --- a/README.zh.md +++ b/README.zh.md @@ -17,7 +17,7 @@ 当前 v0.1 不包含: -- WASM / browser runtime 支持 +- Browser `wasm32-unknown-unknown` runtime 支持 - Rust-only keystore 格式 - 类似 Python `toolkits.py` 的业务工具箱 @@ -32,24 +32,27 @@ | Dynamic contract calls | 已实现 | `call_typed`、`call_json`、`send_typed`、`send_json` | | Proto vendoring pipeline | 已实现 | `scripts/sync_proto.sh` | | 本地节点集成测试脚手架 | 已实现 | 默认 ignored,需要手动启用 | -| WASM 支持 | 规划中 | v1 之后处理 | +| `wasm32-wasip2` 自定义 provider 支持 | 已实现 | 使用 `default-features = false` + `AElfClient::with_provider(...)` | +| Browser `wasm32-unknown-unknown` 支持 | 规划中 | v1 之后处理 | ## Install -在 crate 发布到 crates.io 之前,建议先用 path 或 git 依赖。 +已发布版本建议直接走 crates.io;如果需要使用当前 workspace 的最新代码,再走 path 依赖。 + +当前 crates.io 上已经发布的版本是 `0.1.0-alpha.0`。这条开发线里的 `0.1.0-alpha.1` 包含新的 `wasm32-wasip2` custom-provider 能力,在正式发布前请通过 path 依赖接入。 ```toml [dependencies] -aelf-sdk = { path = "crates/aelf-sdk" } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +aelf-sdk = "0.1.0-alpha.0" +tokio = { version = "1", features = ["macros", "rt"] } ``` -Git 依赖: +Path 依赖: ```toml [dependencies] -aelf-sdk = { git = "https://github.com/AElfProject/aelf-web3.rust", tag = "v0.1.0-alpha.0" } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +aelf-sdk = { path = "crates/aelf-sdk" } +tokio = { version = "1", features = ["macros", "rt"] } ``` ## Quick Start @@ -190,7 +193,7 @@ async fn main() -> Result<(), Box> { ## Examples -示例文件保留在 `/examples`,由 `aelf-sdk` crate 显式挂载编译。 +真正的示例源码现在只保留在 `crates/aelf-sdk/examples`。根目录 `/examples` 只做薄包装转发,避免再维护两套实现。 ```bash cargo run -p aelf-sdk --example basic_client @@ -213,11 +216,70 @@ cargo run -p aelf-sdk --example raw_transaction_flow ## Feature Flags -v0.1 alpha 故意保持最小表面,目前没有额外的 Cargo feature flag。 +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(...)` 构造。 + +## Native WASM (`wasm32-wasip2`) + +`aelf-sdk` 现在可以被 Portkey 这一类 `wasm32-wasip2` native-wasm skill runtime 消费。 -- Runtime:仅 Tokio -- TLS:`reqwest + rustls` -- Proto JSON:`pbjson` +WASM consumer 推荐关闭 native HTTP: + +```toml +[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: + +```rust +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, + ) -> Result { + Err(AElfError::request("host transport not wired", None)) + } + + async fn request_text( + &self, + _method: Method, + _path: &str, + _query: &[(&str, String)], + _body: Option, + ) -> Result { + 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 @@ -245,7 +307,9 @@ let no_retry = AElfClient::new( # let _ = (client, no_retry); ``` -dynamic contract 的 descriptor 现在使用内存内 `64` 容量的 LRU cache,避免长时间运行的服务无限增长。 +`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 @@ -254,6 +318,9 @@ dynamic contract 的 descriptor 现在使用内存内 `64` 容量的 LRU cache ```bash 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 ``` 运行单元测试: @@ -262,6 +329,14 @@ cargo check --workspace --examples cargo test --workspace ``` +默认的 workspace test 故意不包含 ignored 的公网 live smoke,这样本地和 CI 的主测试集仍然保持确定性。 + +运行公网 readonly smoke: + +```bash +cargo test -p aelf-sdk --test public_readonly_smoke -- --ignored --test-threads=1 --nocapture +``` + 运行本地节点集成测试: ```bash @@ -275,6 +350,14 @@ ignored 测试依赖以下环境变量: - `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 日验证以下公网节点: diff --git a/crates/aelf-client/Cargo.toml b/crates/aelf-client/Cargo.toml index ce9cd4c..0a95a98 100644 --- a/crates/aelf-client/Cargo.toml +++ b/crates/aelf-client/Cargo.toml @@ -7,16 +7,20 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = ["native-http"] +native-http = ["dep:reqwest"] + [dependencies] -aelf-crypto = { path = "../aelf-crypto", version = "=0.1.0-alpha.0" } -aelf-proto = { path = "../aelf-proto", version = "=0.1.0-alpha.0" } +aelf-crypto = { path = "../aelf-crypto", version = "=0.1.0-alpha.1" } +aelf-proto = { path = "../aelf-proto", version = "=0.1.0-alpha.1" } async-trait.workspace = true base64.workspace = true hex.workspace = true http.workspace = true pbjson-types.workspace = true prost.workspace = true -reqwest.workspace = true +reqwest = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true thiserror.workspace = true @@ -24,6 +28,7 @@ tokio.workspace = true zeroize.workspace = true [dev-dependencies] +tokio.workspace = true wiremock = "=0.6.4" [lints] diff --git a/crates/aelf-client/src/error.rs b/crates/aelf-client/src/error.rs index 698f2d8..5dc584c 100644 --- a/crates/aelf-client/src/error.rs +++ b/crates/aelf-client/src/error.rs @@ -18,6 +18,8 @@ pub struct RequestError { pub enum AElfError { #[error(transparent)] Request(Box), + #[error("unexpected response: {0}")] + UnexpectedResponse(String), #[error("invalid config: {0}")] InvalidConfig(String), #[error("missing field: {0}")] @@ -30,6 +32,7 @@ pub enum AElfError { Base64(#[from] base64::DecodeError), #[error("json error: {0}")] Json(#[from] serde_json::Error), + #[cfg(feature = "native-http")] #[error("http error: {0}")] Http(#[from] reqwest::Error), #[error("protobuf encode error: {0}")] @@ -50,6 +53,7 @@ impl AElfError { })) } + #[cfg(feature = "native-http")] pub(crate) fn from_response( endpoint: String, status: reqwest::StatusCode, diff --git a/crates/aelf-client/src/lib.rs b/crates/aelf-client/src/lib.rs index 4fdb1da..b75dddd 100644 --- a/crates/aelf-client/src/lib.rs +++ b/crates/aelf-client/src/lib.rs @@ -14,6 +14,7 @@ mod tests; pub use crate::error::AElfError; +#[cfg(feature = "native-http")] use crate::config::ClientConfig; use crate::dto::{ BlockDto, CalculateTransactionFeeInput, CalculateTransactionFeeOutput, ChainStatusDto, @@ -22,15 +23,17 @@ use crate::dto::{ SendTransactionOutput, TaskQueueInfoDto, TransactionPoolStatusOutput, TransactionResultDto, }; use crate::protobuf::RawBytesMessage; -use crate::provider::{HttpProvider, Provider}; +#[cfg(feature = "native-http")] +use crate::provider::HttpProvider; +use crate::provider::Provider; use aelf_crypto::{ address_from_public_key, address_to_pb, base58_to_chain_id, decode_address, pb_to_address, sha256_bytes, sign_transaction, Wallet, }; use aelf_proto::aelf::{Address, Hash, Transaction}; use base64::Engine; +use http::Method; use prost::Message; -use reqwest::Method; use std::fmt; use std::sync::Arc; use zeroize::Zeroize; @@ -40,6 +43,43 @@ const NET_API_BASE: &str = "api/net"; const READONLY_PRIVATE_KEY: &str = "0000000000000000000000000000000000000000000000000000000000000001"; +fn strip_transaction_id_quotes(value: &str) -> &str { + let trimmed = value.trim(); + trimmed + .strip_prefix('"') + .and_then(|unquoted| unquoted.strip_suffix('"')) + .unwrap_or(trimmed) +} + +fn is_valid_transaction_id(value: &str) -> bool { + let candidate = strip_transaction_id_quotes(value); + let candidate = candidate + .strip_prefix("0x") + .or_else(|| candidate.strip_prefix("0X")) + .unwrap_or(candidate); + candidate.len() == 64 && candidate.chars().all(|char| char.is_ascii_hexdigit()) +} + +fn validate_transaction_id( + transaction_id: impl Into, + raw_response: &str, +) -> Result { + let transaction_id = transaction_id.into(); + let transaction_id = strip_transaction_id_quotes(&transaction_id).to_owned(); + if transaction_id.is_empty() { + return Err(AElfError::UnexpectedResponse( + "empty sendTransaction response".to_owned(), + )); + } + if is_valid_transaction_id(&transaction_id) { + Ok(transaction_id) + } else { + Err(AElfError::UnexpectedResponse(format!( + "sendTransaction returned a non-transaction id payload: {raw_response}" + ))) + } +} + /// Async HTTP client used by the facade crate and lower-level integrations. #[derive(Clone)] pub struct AElfClient { @@ -48,6 +88,7 @@ pub struct AElfClient { impl AElfClient { /// Creates a client backed by the default HTTP provider. + #[cfg(feature = "native-http")] pub fn new(config: ClientConfig) -> Result { Self::with_provider(HttpProvider::new(config)?) } @@ -342,23 +383,21 @@ impl TransactionService { serde_json::json!({ "RawTransaction": raw_transaction }), ) .await?; - serde_json::from_str::(&text) - .or_else(|_| { - serde_json::from_str::(&text) - .map(|transaction_id| SendTransactionOutput { transaction_id }) - }) - .or_else(|_| { - let transaction_id = text.trim().trim_matches('"').to_owned(); - if transaction_id.is_empty() { - Err(serde_json::Error::io(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "empty sendTransaction response", - ))) - } else { - Ok(SendTransactionOutput { transaction_id }) - } - }) - .map_err(AElfError::Json) + if let Ok(output) = serde_json::from_str::(&text) { + return Ok(SendTransactionOutput { + transaction_id: validate_transaction_id(output.transaction_id, &text)?, + }); + } + + if let Ok(transaction_id) = serde_json::from_str::(&text) { + return Ok(SendTransactionOutput { + transaction_id: validate_transaction_id(transaction_id, &text)?, + }); + } + + Ok(SendTransactionOutput { + transaction_id: validate_transaction_id(&text, &text)?, + }) } pub async fn send_transactions( diff --git a/crates/aelf-client/src/provider.rs b/crates/aelf-client/src/provider.rs index cb6f397..aca6bc1 100644 --- a/crates/aelf-client/src/provider.rs +++ b/crates/aelf-client/src/provider.rs @@ -1,9 +1,14 @@ +#[cfg(feature = "native-http")] use crate::config::{ClientConfig, RetryPolicy}; use crate::error::AElfError; use async_trait::async_trait; +use http::Method; +#[cfg(feature = "native-http")] use reqwest::header::{ACCEPT, CONTENT_TYPE}; -use reqwest::{Method, StatusCode}; +#[cfg(feature = "native-http")] +use reqwest::StatusCode; use serde_json::Value; +#[cfg(feature = "native-http")] use std::time::Duration; /// Abstract transport used by the SDK client. @@ -29,12 +34,14 @@ pub trait Provider: Send + Sync { } /// Default HTTP transport backed by `reqwest`. +#[cfg(feature = "native-http")] #[derive(Clone, Debug)] pub struct HttpProvider { config: ClientConfig, client: reqwest::Client, } +#[cfg(feature = "native-http")] impl HttpProvider { /// Creates a new HTTP transport from client configuration. pub fn new(config: ClientConfig) -> Result { @@ -113,6 +120,7 @@ impl HttpProvider { } } +#[cfg(feature = "native-http")] #[async_trait] impl Provider for HttpProvider { async fn request_json( @@ -346,7 +354,7 @@ mod test_support { } } -#[cfg(test)] +#[cfg(all(test, feature = "native-http"))] mod tests { use super::*; use base64::Engine; diff --git a/crates/aelf-client/src/tests.rs b/crates/aelf-client/src/tests.rs index a019b1f..11b4859 100644 --- a/crates/aelf-client/src/tests.rs +++ b/crates/aelf-client/src/tests.rs @@ -1,9 +1,11 @@ use crate::dto::CreateRawTransactionInput; use crate::provider::{MockCallKind, MockProvider, MockRecordedRequest, MockResponse}; use crate::{AElfClient, AElfError}; -use reqwest::Method; +use http::Method; use serde_json::{json, Value}; +const VALID_TX_ID: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + fn mock_client(responses: Vec) -> (AElfClient, MockProvider) { let provider = MockProvider::new(responses); let client = AElfClient::with_provider(provider.clone()).expect("client"); @@ -34,8 +36,9 @@ fn assert_single_request(provider: &MockProvider) -> MockRecordedRequest { #[tokio::test] async fn send_transaction_accepts_object_response() { - let (client, provider) = - mock_client(vec![MockResponse::text(r#"{"TransactionId":"tx-object"}"#)]); + let (client, provider) = mock_client(vec![MockResponse::text(format!( + r#"{{"TransactionId":"{VALID_TX_ID}"}}"# + ))]); let output = client .tx() @@ -43,7 +46,7 @@ async fn send_transaction_accepts_object_response() { .await .expect("send transaction"); - assert_eq!(output.transaction_id, "tx-object"); + assert_eq!(output.transaction_id, VALID_TX_ID); let request = assert_single_request(&provider); assert_eq!(request.kind, MockCallKind::Text); @@ -57,7 +60,7 @@ async fn send_transaction_accepts_object_response() { #[tokio::test] async fn send_transaction_accepts_json_string_response() { - let (client, _) = mock_client(vec![MockResponse::text(r#""tx-string""#)]); + let (client, _) = mock_client(vec![MockResponse::text(format!(r#""{VALID_TX_ID}""#))]); let output = client .tx() @@ -65,12 +68,25 @@ async fn send_transaction_accepts_json_string_response() { .await .expect("send transaction"); - assert_eq!(output.transaction_id, "tx-string"); + assert_eq!(output.transaction_id, VALID_TX_ID); } #[tokio::test] async fn send_transaction_accepts_plain_text_response() { - let (client, _) = mock_client(vec![MockResponse::text("tx-plain")]); + let (client, _) = mock_client(vec![MockResponse::text(VALID_TX_ID)]); + + let output = client + .tx() + .send_transaction("raw-transaction") + .await + .expect("send transaction"); + + assert_eq!(output.transaction_id, VALID_TX_ID); +} + +#[tokio::test] +async fn send_transaction_accepts_0x_prefixed_plain_text_response() { + let (client, _) = mock_client(vec![MockResponse::text(format!("0x{VALID_TX_ID}"))]); let output = client .tx() @@ -78,7 +94,7 @@ async fn send_transaction_accepts_plain_text_response() { .await .expect("send transaction"); - assert_eq!(output.transaction_id, "tx-plain"); + assert_eq!(output.transaction_id, format!("0x{VALID_TX_ID}")); } #[tokio::test] @@ -91,7 +107,33 @@ async fn send_transaction_rejects_empty_response() { .await .expect_err("empty response should fail"); - assert!(matches!(error, AElfError::Json(_))); + assert!(matches!(error, AElfError::UnexpectedResponse(_))); +} + +#[tokio::test] +async fn send_transaction_rejects_non_txid_plain_text_response() { + let (client, _) = mock_client(vec![MockResponse::text("tx-plain")]); + + let error = client + .tx() + .send_transaction("raw-transaction") + .await + .expect_err("non-txid response should fail"); + + assert!(matches!(error, AElfError::UnexpectedResponse(_))); +} + +#[tokio::test] +async fn send_transaction_rejects_ok_plain_text_response() { + let (client, _) = mock_client(vec![MockResponse::text("ok")]); + + let error = client + .tx() + .send_transaction("raw-transaction") + .await + .expect_err("ok response should fail"); + + assert!(matches!(error, AElfError::UnexpectedResponse(_))); } #[tokio::test] diff --git a/crates/aelf-contract/Cargo.toml b/crates/aelf-contract/Cargo.toml index 9f66d5f..f73c034 100644 --- a/crates/aelf-contract/Cargo.toml +++ b/crates/aelf-contract/Cargo.toml @@ -7,13 +7,16 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = ["native-http"] +native-http = ["aelf-client/native-http"] + [dependencies] -aelf-client = { path = "../aelf-client", version = "=0.1.0-alpha.0" } -aelf-crypto = { path = "../aelf-crypto", version = "=0.1.0-alpha.0" } -aelf-proto = { path = "../aelf-proto", version = "=0.1.0-alpha.0" } +aelf-client = { path = "../aelf-client", version = "=0.1.0-alpha.1", default-features = false } +aelf-crypto = { path = "../aelf-crypto", version = "=0.1.0-alpha.1" } +aelf-proto = { path = "../aelf-proto", version = "=0.1.0-alpha.1" } bytes.workspace = true hex.workspace = true -lru.workspace = true pbjson-types.workspace = true prost.workspace = true prost-reflect.workspace = true @@ -22,5 +25,10 @@ serde_json.workspace = true thiserror.workspace = true tokio.workspace = true +[dev-dependencies] +async-trait.workspace = true +base64.workspace = true +http.workspace = true + [lints] workspace = true diff --git a/crates/aelf-contract/src/lib.rs b/crates/aelf-contract/src/lib.rs index 2d570c3..4ac0c8b 100644 --- a/crates/aelf-contract/src/lib.rs +++ b/crates/aelf-contract/src/lib.rs @@ -7,27 +7,13 @@ use aelf_client::protobuf::RawBytesMessage; use aelf_client::{AElfClient, AElfError}; use aelf_crypto::{address_to_pb, hash_to_pb, pb_to_address, Wallet}; use aelf_proto::{aedpos, cross_chain, election, token, vote}; -use lru::LruCache; use prost::Message; use prost_reflect::{DescriptorPool, DynamicMessage, Kind, MessageDescriptor, MethodDescriptor}; use serde::de::IntoDeserializer; use serde_json::{Map, Value}; -use std::num::NonZeroUsize; -use std::sync::OnceLock; +use std::sync::Arc; use thiserror::Error; -use tokio::sync::RwLock; - -const DESCRIPTOR_CACHE_CAPACITY: usize = 64; - -static DESCRIPTOR_CACHE: OnceLock>> = OnceLock::new(); - -fn descriptor_cache() -> &'static RwLock> { - DESCRIPTOR_CACHE.get_or_init(|| { - RwLock::new(LruCache::new( - NonZeroUsize::new(DESCRIPTOR_CACHE_CAPACITY).expect("cache capacity must be non-zero"), - )) - }) -} +use tokio::sync::OnceCell; /// Errors returned by typed and dynamic contract operations. #[derive(Debug, Error)] @@ -71,26 +57,11 @@ impl DynamicContract { wallet: Wallet, ) -> Result { let address = address.into(); - let pool = { - let mut cache = descriptor_cache().write().await; - cache.get(&address).cloned() - }; - - let pool = match pool { - Some(pool) => pool, - None => { - let bytes = client - .chain() - .get_contract_file_descriptor_set(&address) - .await?; - let pool = DescriptorPool::decode(bytes.as_slice())?; - descriptor_cache() - .write() - .await - .put(address.clone(), pool.clone()); - pool - } - }; + let bytes = client + .chain() + .get_contract_file_descriptor_set(&address) + .await?; + let pool = DescriptorPool::decode(bytes.as_slice())?; Ok(Self { client, @@ -369,6 +340,40 @@ async fn build_signed_raw( Ok(hex::encode(transaction.encode_to_vec())) } +#[derive(Clone)] +struct LazyDynamicContract { + client: AElfClient, + wallet: Wallet, + address: String, + dynamic: Arc>, +} + +impl LazyDynamicContract { + fn new(client: AElfClient, wallet: Wallet, address: impl Into) -> Self { + Self { + client, + wallet, + address: address.into(), + dynamic: Arc::new(OnceCell::new()), + } + } + + async fn get(&self) -> Result { + let contract = self + .dynamic + .get_or_try_init(|| async { + DynamicContract::at( + self.client.clone(), + self.address.clone(), + self.wallet.clone(), + ) + .await + }) + .await?; + Ok(contract.clone()) + } +} + /// Typed wrapper for the genesis zero contract. #[derive(Clone)] pub struct ZeroContract { @@ -413,18 +418,14 @@ impl ZeroContract { /// Typed wrapper for the token contract. #[derive(Clone)] pub struct TokenContract { - client: AElfClient, - wallet: Wallet, - address: String, + dynamic: LazyDynamicContract, } impl TokenContract { /// Creates a typed token contract wrapper. pub fn new(client: AElfClient, wallet: Wallet, address: impl Into) -> Self { Self { - client, - wallet, - address: address.into(), + dynamic: LazyDynamicContract::new(client, wallet, address), } } @@ -485,30 +486,21 @@ impl TokenContract { } async fn dynamic(&self) -> Result { - DynamicContract::at( - self.client.clone(), - self.address.clone(), - self.wallet.clone(), - ) - .await + self.dynamic.get().await } } /// Typed wrapper for the election contract. #[derive(Clone)] pub struct ElectionContract { - client: AElfClient, - wallet: Wallet, - address: String, + dynamic: LazyDynamicContract, } impl ElectionContract { /// Creates a typed election contract wrapper. pub fn new(client: AElfClient, wallet: Wallet, address: impl Into) -> Self { Self { - client, - wallet, - address: address.into(), + dynamic: LazyDynamicContract::new(client, wallet, address), } } @@ -555,30 +547,21 @@ impl ElectionContract { } async fn dynamic(&self) -> Result { - DynamicContract::at( - self.client.clone(), - self.address.clone(), - self.wallet.clone(), - ) - .await + self.dynamic.get().await } } /// Typed wrapper for the vote contract. #[derive(Clone)] pub struct VoteContract { - client: AElfClient, - wallet: Wallet, - address: String, + dynamic: LazyDynamicContract, } impl VoteContract { /// Creates a typed vote contract wrapper. pub fn new(client: AElfClient, wallet: Wallet, address: impl Into) -> Self { Self { - client, - wallet, - address: address.into(), + dynamic: LazyDynamicContract::new(client, wallet, address), } } @@ -616,30 +599,21 @@ impl VoteContract { } async fn dynamic(&self) -> Result { - DynamicContract::at( - self.client.clone(), - self.address.clone(), - self.wallet.clone(), - ) - .await + self.dynamic.get().await } } /// Typed wrapper for the cross-chain contract. #[derive(Clone)] pub struct CrossChainContract { - client: AElfClient, - wallet: Wallet, - address: String, + dynamic: LazyDynamicContract, } impl CrossChainContract { /// Creates a typed cross-chain contract wrapper. pub fn new(client: AElfClient, wallet: Wallet, address: impl Into) -> Self { Self { - client, - wallet, - address: address.into(), + dynamic: LazyDynamicContract::new(client, wallet, address), } } @@ -691,30 +665,21 @@ impl CrossChainContract { } async fn dynamic(&self) -> Result { - DynamicContract::at( - self.client.clone(), - self.address.clone(), - self.wallet.clone(), - ) - .await + self.dynamic.get().await } } /// Typed wrapper for the AEDPoS consensus contract. #[derive(Clone)] pub struct AedposContract { - client: AElfClient, - wallet: Wallet, - address: String, + dynamic: LazyDynamicContract, } impl AedposContract { /// Creates a typed AEDPoS contract wrapper. pub fn new(client: AElfClient, wallet: Wallet, address: impl Into) -> Self { Self { - client, - wallet, - address: address.into(), + dynamic: LazyDynamicContract::new(client, wallet, address), } } @@ -737,19 +702,22 @@ impl AedposContract { } async fn dynamic(&self) -> Result { - DynamicContract::at( - self.client.clone(), - self.address.clone(), - self.wallet.clone(), - ) - .await + self.dynamic.get().await } } #[cfg(test)] mod tests { use super::*; + use aelf_client::provider::Provider; + use async_trait::async_trait; + use base64::Engine; + use http::Method; use serde_json::json; + use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }; const READONLY_PRIVATE_KEY: &str = "0000000000000000000000000000000000000000000000000000000000000001"; @@ -810,42 +778,107 @@ mod tests { assert_eq!(normalized.get("symbol"), Some(&json!("ELF"))); } - #[tokio::test] - async fn descriptor_cache_evicts_oldest_entry_when_capacity_is_exceeded() { - let pool = DescriptorPool::decode(aelf_proto::FILE_DESCRIPTOR_SET).expect("descriptor"); - let mut cache = descriptor_cache().write().await; - cache.clear(); + #[derive(Clone)] + struct CountingDescriptorProvider { + requests: Arc, + } + + #[async_trait] + impl Provider for CountingDescriptorProvider { + async fn request_json( + &self, + _method: Method, + _path: &str, + _query: &[(&str, String)], + _body: Option, + ) -> Result { + Err(AElfError::request( + "unexpected JSON request in descriptor test", + None, + )) + } - for index in 0..=DESCRIPTOR_CACHE_CAPACITY { - cache.put(format!("contract-{index}"), pool.clone()); + async fn request_text( + &self, + method: Method, + path: &str, + query: &[(&str, String)], + _body: Option, + ) -> Result { + assert_eq!(method, Method::GET); + assert_eq!(path, "api/blockChain/contractFileDescriptorSet"); + assert_eq!(query, &[("address", "token-contract".to_owned())]); + + self.requests.fetch_add(1, Ordering::SeqCst); + Ok(format!( + "\"{}\"", + base64::engine::general_purpose::STANDARD.encode(aelf_proto::FILE_DESCRIPTOR_SET) + )) } + } + + #[tokio::test] + async fn dynamic_contract_fetches_descriptor_on_every_at_call() { + let requests = Arc::new(AtomicUsize::new(0)); + let client = AElfClient::with_provider(CountingDescriptorProvider { + requests: requests.clone(), + }) + .expect("client"); + let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY).expect("wallet"); - assert_eq!(cache.len(), DESCRIPTOR_CACHE_CAPACITY); - assert!(cache.peek("contract-0").is_none()); - assert!(cache - .peek(&format!("contract-{DESCRIPTOR_CACHE_CAPACITY}")) - .is_some()); - cache.clear(); + let first = DynamicContract::at(client.clone(), "token-contract", wallet.clone()) + .await + .expect("first contract"); + let second = DynamicContract::at(client, "token-contract", wallet) + .await + .expect("second contract"); + + assert!(first.method("GetBalance").is_ok()); + assert!(second.method("GetBalance").is_ok()); + assert_eq!(requests.load(Ordering::SeqCst), 2); } #[tokio::test] - async fn descriptor_cache_refreshes_recently_accessed_entry() { - let pool = DescriptorPool::decode(aelf_proto::FILE_DESCRIPTOR_SET).expect("descriptor"); - let mut cache = descriptor_cache().write().await; - cache.clear(); + async fn typed_wrapper_reuses_descriptor_within_same_handle() { + let requests = Arc::new(AtomicUsize::new(0)); + let client = AElfClient::with_provider(CountingDescriptorProvider { + requests: requests.clone(), + }) + .expect("client"); + let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY).expect("wallet"); + let token = TokenContract::new(client, wallet, "token-contract"); - for index in 0..DESCRIPTOR_CACHE_CAPACITY { - cache.put(format!("contract-{index}"), pool.clone()); - } + let first = token.dynamic().await.expect("first dynamic"); + let second = token.dynamic().await.expect("second dynamic"); - assert!(cache.get("contract-0").is_some()); - cache.put( - format!("contract-{DESCRIPTOR_CACHE_CAPACITY}"), - pool.clone(), - ); + assert!(first.method("GetBalance").is_ok()); + assert!(second.method("GetBalance").is_ok()); + assert_eq!(requests.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn typed_wrapper_clone_reuses_descriptor_cache() { + let requests = Arc::new(AtomicUsize::new(0)); + let client = AElfClient::with_provider(CountingDescriptorProvider { + requests: requests.clone(), + }) + .expect("client"); + let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY).expect("wallet"); + let first = TokenContract::new(client, wallet, "token-contract"); + let second = first.clone(); - assert!(cache.peek("contract-0").is_some()); - assert!(cache.peek("contract-1").is_none()); - cache.clear(); + assert!(first + .dynamic() + .await + .expect("first dynamic") + .method("GetBalance") + .is_ok()); + assert!(second + .dynamic() + .await + .expect("second dynamic") + .method("GetBalance") + .is_ok()); + assert_eq!(requests.load(Ordering::SeqCst), 1); } } diff --git a/crates/aelf-crypto/Cargo.toml b/crates/aelf-crypto/Cargo.toml index 7126a1b..37aa8e9 100644 --- a/crates/aelf-crypto/Cargo.toml +++ b/crates/aelf-crypto/Cargo.toml @@ -8,7 +8,7 @@ rust-version.workspace = true version.workspace = true [dependencies] -aelf-proto = { path = "../aelf-proto", version = "=0.1.0-alpha.0" } +aelf-proto = { path = "../aelf-proto", version = "=0.1.0-alpha.1" } bs58.workspace = true coins-bip32.workspace = true coins-bip39.workspace = true diff --git a/crates/aelf-keystore/Cargo.toml b/crates/aelf-keystore/Cargo.toml index 2d2f914..f1f6181 100644 --- a/crates/aelf-keystore/Cargo.toml +++ b/crates/aelf-keystore/Cargo.toml @@ -8,7 +8,7 @@ rust-version.workspace = true version.workspace = true [dependencies] -aelf-crypto = { path = "../aelf-crypto", version = "=0.1.0-alpha.0" } +aelf-crypto = { path = "../aelf-crypto", version = "=0.1.0-alpha.1" } aes.workspace = true cbc.workspace = true cipher.workspace = true diff --git a/crates/aelf-sdk/Cargo.toml b/crates/aelf-sdk/Cargo.toml index fabea82..529b588 100644 --- a/crates/aelf-sdk/Cargo.toml +++ b/crates/aelf-sdk/Cargo.toml @@ -7,12 +7,16 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = ["native-http"] +native-http = ["aelf-client/native-http", "aelf-contract/native-http"] + [dependencies] -aelf-client = { path = "../aelf-client", version = "=0.1.0-alpha.0" } -aelf-contract = { path = "../aelf-contract", version = "=0.1.0-alpha.0" } -aelf-crypto = { path = "../aelf-crypto", version = "=0.1.0-alpha.0" } -aelf-keystore = { path = "../aelf-keystore", version = "=0.1.0-alpha.0" } -aelf-proto = { path = "../aelf-proto", version = "=0.1.0-alpha.0" } +aelf-client = { path = "../aelf-client", version = "=0.1.0-alpha.1", default-features = false } +aelf-contract = { path = "../aelf-contract", version = "=0.1.0-alpha.1", default-features = false } +aelf-crypto = { path = "../aelf-crypto", version = "=0.1.0-alpha.1" } +aelf-keystore = { path = "../aelf-keystore", version = "=0.1.0-alpha.1" } +aelf-proto = { path = "../aelf-proto", version = "=0.1.0-alpha.1" } [dev-dependencies] hex.workspace = true diff --git a/crates/aelf-sdk/examples/basic_client.rs b/crates/aelf-sdk/examples/basic_client.rs index 514fc17..d4317c1 100644 --- a/crates/aelf-sdk/examples/basic_client.rs +++ b/crates/aelf-sdk/examples/basic_client.rs @@ -1,6 +1,6 @@ use aelf_sdk::{AElfClient, ClientConfig}; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { let endpoint = std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); diff --git a/crates/aelf-sdk/examples/dynamic_contract_get_balance.rs b/crates/aelf-sdk/examples/dynamic_contract_get_balance.rs index 365956e..a60822a 100644 --- a/crates/aelf-sdk/examples/dynamic_contract_get_balance.rs +++ b/crates/aelf-sdk/examples/dynamic_contract_get_balance.rs @@ -1,7 +1,7 @@ use aelf_sdk::{AElfClient, ClientConfig, Wallet}; use serde_json::json; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { let endpoint = std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); diff --git a/crates/aelf-sdk/examples/public_balance.rs b/crates/aelf-sdk/examples/public_balance.rs index 0079ce8..09b97b2 100644 --- a/crates/aelf-sdk/examples/public_balance.rs +++ b/crates/aelf-sdk/examples/public_balance.rs @@ -5,7 +5,7 @@ use aelf_sdk::{decode_address, format_token_amount, AElfClient, ClientConfig, Wa const READONLY_PRIVATE_KEY: &str = "0000000000000000000000000000000000000000000000000000000000000001"; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { let endpoint = std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); diff --git a/crates/aelf-sdk/examples/raw_transaction_flow.rs b/crates/aelf-sdk/examples/raw_transaction_flow.rs index fa416c0..3efe901 100644 --- a/crates/aelf-sdk/examples/raw_transaction_flow.rs +++ b/crates/aelf-sdk/examples/raw_transaction_flow.rs @@ -7,7 +7,7 @@ use aelf_sdk::{decode_address, parse_aelf_address, AElfClient, ClientConfig, Wal use prost::Message; use tokio::time::{sleep, Duration}; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { let endpoint = std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); diff --git a/crates/aelf-sdk/examples/token_transfer.rs b/crates/aelf-sdk/examples/token_transfer.rs index 316b1f4..85d3004 100644 --- a/crates/aelf-sdk/examples/token_transfer.rs +++ b/crates/aelf-sdk/examples/token_transfer.rs @@ -3,7 +3,7 @@ use aelf_sdk::{decode_address, parse_aelf_address, AElfClient, ClientConfig, Wal use prost::Message; use tokio::time::{sleep, Duration}; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { let endpoint = std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); diff --git a/crates/aelf-sdk/examples/wallet_keystore_roundtrip.rs b/crates/aelf-sdk/examples/wallet_keystore_roundtrip.rs index 2411af9..604a45b 100644 --- a/crates/aelf-sdk/examples/wallet_keystore_roundtrip.rs +++ b/crates/aelf-sdk/examples/wallet_keystore_roundtrip.rs @@ -6,7 +6,9 @@ fn main() -> Result<(), Box> { let unlocked = keystore.unlock_js("123123")?; println!("address={}", unlocked.address); - println!("private_key={}", unlocked.private_key); - println!("mnemonic={}", unlocked.mnemonic); + println!("keystore_roundtrip=ok"); + println!("warning=private_key_and_mnemonic_are_intentionally_redacted"); + assert_eq!(wallet.private_key(), unlocked.private_key); + assert_eq!(wallet.mnemonic(), unlocked.mnemonic); Ok(()) } diff --git a/crates/aelf-sdk/src/lib.rs b/crates/aelf-sdk/src/lib.rs index 23dd8e6..126713d 100644 --- a/crates/aelf-sdk/src/lib.rs +++ b/crates/aelf-sdk/src/lib.rs @@ -7,6 +7,9 @@ pub use aelf_client::config::{BasicAuth, ClientConfig, RetryPolicy}; pub use aelf_client::dto; +#[cfg(feature = "native-http")] +pub use aelf_client::provider::HttpProvider; +pub use aelf_client::provider::Provider; pub use aelf_client::{AElfError, KeyPairInfo, TransactionBuilder}; pub use aelf_contract::{ AedposContract, ContractError, CrossChainContract, DynamicContract, ElectionContract, @@ -28,12 +31,23 @@ pub struct AElfClient { impl AElfClient { /// Creates a facade client backed by the default HTTP provider. + #[cfg(feature = "native-http")] pub fn new(config: ClientConfig) -> Result { Ok(Self { inner: aelf_client::AElfClient::new(config)?, }) } + /// Creates a facade client from a custom provider implementation. + pub fn with_provider

(provider: P) -> Result + where + P: Provider + 'static, + { + Ok(Self { + inner: aelf_client::AElfClient::with_provider(provider)?, + }) + } + /// Returns block-related APIs. pub fn block(&self) -> aelf_client::BlockService { self.inner.block() diff --git a/crates/aelf-sdk/tests/funded_transaction_smoke.rs b/crates/aelf-sdk/tests/funded_transaction_smoke.rs new file mode 100644 index 0000000..a1c04ea --- /dev/null +++ b/crates/aelf-sdk/tests/funded_transaction_smoke.rs @@ -0,0 +1,74 @@ +use aelf_sdk::proto::token::TransferInput; +use aelf_sdk::{address_to_pb, parse_aelf_address, AElfClient, ClientConfig, Wallet}; +use prost::Message; +use std::env; +use std::error::Error; +use tokio::time::{sleep, Duration}; + +fn required_env(name: &str) -> Result> { + env::var(name).map_err(|_| format!("missing required environment variable: {name}").into()) +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "requires funded account and reachable node"] +async fn funded_send_transaction_smoke() -> Result<(), Box> { + let endpoint = required_env("AELF_ENDPOINT")?; + let private_key = required_env("AELF_PRIVATE_KEY")?; + let to_address = required_env("AELF_TO_ADDRESS")?; + let amount = env::var("AELF_AMOUNT") + .ok() + .map(|value| value.parse::()) + .transpose()? + .unwrap_or(1); + + let client = AElfClient::new(ClientConfig::new(endpoint))?; + let wallet = Wallet::from_private_key(&private_key)?; + let token_address = match env::var("AELF_TOKEN_CONTRACT") { + Ok(value) if !value.is_empty() => value, + _ => { + client + .utils() + .get_contract_address_by_name("AElf.ContractNames.Token") + .await? + } + }; + + let transfer = TransferInput { + to: Some(address_to_pb(parse_aelf_address(&to_address))?), + symbol: "ELF".to_owned(), + amount, + memo: "sdk funded transaction smoke".to_owned(), + }; + + let transaction = client + .transaction_builder() + .with_wallet(wallet) + .with_contract(token_address) + .with_method("Transfer") + .with_message(&transfer) + .build_signed() + .await?; + let raw_transaction = hex::encode(transaction.encode_to_vec()); + + let send_output = client.tx().send_transaction(&raw_transaction).await?; + assert!(!send_output.transaction_id.is_empty()); + + for _ in 0..20 { + let result = client + .tx() + .get_transaction_result(&send_output.transaction_id) + .await?; + if result.status == "MINED" { + return Ok(()); + } + if result.status != "PENDING" { + return Err( + format!("transaction ended in unexpected status: {}", result.status).into(), + ); + } + + sleep(Duration::from_secs(1)).await; + } + + Err("transaction was not mined before timeout".into()) +} diff --git a/crates/aelf-sdk/tests/public_readonly_smoke.rs b/crates/aelf-sdk/tests/public_readonly_smoke.rs new file mode 100644 index 0000000..7662dad --- /dev/null +++ b/crates/aelf-sdk/tests/public_readonly_smoke.rs @@ -0,0 +1,67 @@ +use aelf_sdk::proto::token::GetBalanceInput; +use aelf_sdk::{address_to_pb, AElfClient, ClientConfig, Wallet}; +use serde_json::json; +use std::error::Error; + +const READONLY_PRIVATE_KEY: &str = + "0000000000000000000000000000000000000000000000000000000000000001"; +const MAIN_CHAIN_ENDPOINT: &str = "https://aelf-public-node.aelf.io"; +const SIDE_CHAIN_ENDPOINT: &str = "https://tdvv-public-node.aelf.io"; + +async fn assert_public_readonly_smoke( + endpoint: &str, + expected_chain_id: &str, +) -> Result<(), Box> { + let client = AElfClient::new(ClientConfig::new(endpoint))?; + let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY)?; + + let status = client.chain().get_chain_status().await?; + assert_eq!(status.chain_id, expected_chain_id); + assert!(status.best_chain_height > 0); + assert!(!status.genesis_contract_address.is_empty()); + + let token_address = client + .utils() + .get_contract_address_by_name("AElf.ContractNames.Token") + .await?; + assert!(!token_address.is_empty()); + + let token = client.token_contract(token_address.clone(), wallet.clone()); + let primary_symbol = token.get_primary_token_symbol().await?; + let native = token.get_native_token_info().await?; + assert_eq!(native.symbol, primary_symbol); + + let balance = token + .get_balance(&GetBalanceInput { + symbol: primary_symbol.clone(), + owner: Some(address_to_pb(wallet.address())?), + }) + .await?; + assert_eq!(balance.symbol, primary_symbol); + + let dynamic = client.contract_at(token_address, wallet.clone()).await?; + let dynamic_balance = dynamic + .call_json( + "GetBalance", + json!({ + "symbol": balance.symbol, + "owner": wallet.address(), + }), + ) + .await?; + assert_eq!(dynamic_balance.get("owner"), Some(&json!(wallet.address()))); + + Ok(()) +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "requires live public nodes"] +async fn public_main_chain_readonly_smoke() -> Result<(), Box> { + assert_public_readonly_smoke(MAIN_CHAIN_ENDPOINT, "AELF").await +} + +#[tokio::test(flavor = "current_thread")] +#[ignore = "requires live public nodes"] +async fn public_side_chain_readonly_smoke() -> Result<(), Box> { + assert_public_readonly_smoke(SIDE_CHAIN_ENDPOINT, "tDVV").await +} diff --git a/examples/basic_client.rs b/examples/basic_client.rs index 514fc17..3ee5d94 100644 --- a/examples/basic_client.rs +++ b/examples/basic_client.rs @@ -1,14 +1 @@ -use aelf_sdk::{AElfClient, ClientConfig}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let endpoint = - std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); - let client = AElfClient::new(ClientConfig::new(endpoint))?; - 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(()) -} +include!("../crates/aelf-sdk/examples/basic_client.rs"); diff --git a/examples/dynamic_contract_get_balance.rs b/examples/dynamic_contract_get_balance.rs index 365956e..eceaa3a 100644 --- a/examples/dynamic_contract_get_balance.rs +++ b/examples/dynamic_contract_get_balance.rs @@ -1,29 +1 @@ -use aelf_sdk::{AElfClient, ClientConfig, Wallet}; -use serde_json::json; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let endpoint = - std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); - let token_address = std::env::var("AELF_TOKEN_CONTRACT")?; - let owner = std::env::var("AELF_OWNER_ADDRESS")?; - let private_key = std::env::var("AELF_PRIVATE_KEY").unwrap_or_else(|_| { - "0000000000000000000000000000000000000000000000000000000000000001".to_owned() - }); - - let client = AElfClient::new(ClientConfig::new(endpoint))?; - let wallet = Wallet::from_private_key(&private_key)?; - let contract = client.contract_at(token_address, wallet).await?; - let balance = contract - .call_json( - "GetBalance", - json!({ - "symbol": "ELF", - "owner": owner, - }), - ) - .await?; - - println!("{balance}"); - Ok(()) -} +include!("../crates/aelf-sdk/examples/dynamic_contract_get_balance.rs"); diff --git a/examples/public_balance.rs b/examples/public_balance.rs index 0079ce8..171f6ba 100644 --- a/examples/public_balance.rs +++ b/examples/public_balance.rs @@ -1,39 +1 @@ -use aelf_sdk::proto::aelf::Address; -use aelf_sdk::proto::token::GetBalanceInput; -use aelf_sdk::{decode_address, format_token_amount, AElfClient, ClientConfig, Wallet}; - -const READONLY_PRIVATE_KEY: &str = - "0000000000000000000000000000000000000000000000000000000000000001"; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let endpoint = - std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); - let owner = std::env::var("AELF_OWNER_ADDRESS")?; - let client = AElfClient::new(ClientConfig::new(endpoint))?; - let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY)?; - let token_address = client - .utils() - .get_contract_address_by_name("AElf.ContractNames.Token") - .await?; - let token = client.token_contract(token_address.clone(), wallet); - let token_info = token.get_native_token_info().await?; - let balance = token - .get_balance(&GetBalanceInput { - symbol: token_info.symbol.clone(), - owner: Some(Address { - value: decode_address(&owner)?, - }), - }) - .await?; - - println!("token_contract={token_address}"); - println!("symbol={}", token_info.symbol); - println!("decimals={}", token_info.decimals); - println!("raw_balance={}", balance.balance); - println!( - "display_balance={}", - format_token_amount(balance.balance, token_info.decimals) - ); - Ok(()) -} +include!("../crates/aelf-sdk/examples/public_balance.rs"); diff --git a/examples/raw_transaction_flow.rs b/examples/raw_transaction_flow.rs index fa416c0..f3d15d5 100644 --- a/examples/raw_transaction_flow.rs +++ b/examples/raw_transaction_flow.rs @@ -1,135 +1 @@ -use aelf_sdk::dto::{ - CalculateTransactionFeeInput, CreateRawTransactionInput, ExecuteRawTransactionDto, - SendRawTransactionInput, -}; -use aelf_sdk::proto::token::TransferInput; -use aelf_sdk::{decode_address, parse_aelf_address, AElfClient, ClientConfig, Wallet}; -use prost::Message; -use tokio::time::{sleep, Duration}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let endpoint = - std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); - let private_key = std::env::var("AELF_PRIVATE_KEY")?; - let to_env = std::env::var("AELF_TO_ADDRESS")?; - let to = parse_aelf_address(&to_env).to_owned(); - let amount = std::env::var("AELF_AMOUNT") - .ok() - .map(|value| value.parse::()) - .transpose()? - .unwrap_or(1); - let should_send = matches!( - std::env::var("AELF_SEND").ok().as_deref(), - Some("1" | "true" | "TRUE" | "yes" | "YES") - ); - let client = AElfClient::new(ClientConfig::new(endpoint))?; - let wallet = Wallet::from_private_key(&private_key)?; - let token_address = client - .utils() - .get_contract_address_by_name("AElf.ContractNames.Token") - .await?; - let chain_status = client.chain().get_chain_status().await?; - let transfer = TransferInput { - to: Some(aelf_sdk::proto::aelf::Address { - value: decode_address(&to)?, - }), - symbol: "ELF".to_owned(), - amount, - memo: "raw transaction flow validation".to_owned(), - }; - let params_json = serde_json::to_string(&transfer)?; - - let unsigned = client - .utils() - .generate_transaction(wallet.address(), &token_address, "Transfer", &transfer) - .await?; - let signed = client.utils().sign_transaction(&wallet, unsigned)?; - let mut unsigned_only = signed.clone(); - unsigned_only.signature.clear(); - let unsigned_raw = hex::encode(unsigned_only.encode_to_vec()); - let created = client - .tx() - .create_raw_transaction(&CreateRawTransactionInput { - from: wallet.address().to_owned(), - to: token_address.clone(), - ref_block_number: chain_status.best_chain_height, - ref_block_hash: chain_status.best_chain_hash.clone(), - method_name: "Transfer".to_owned(), - params: params_json, - }) - .await; - let (raw_transaction, created_by_node) = match created { - Ok(created) => (created.raw_transaction, true), - Err(error) => { - println!("create_raw_transaction_error={error}"); - (unsigned_raw.clone(), false) - } - }; - let signature = hex::encode(wallet.sign(&hex::decode(&raw_transaction)?)?); - - println!("from={}", wallet.address()); - println!("to={to}"); - println!( - "formatted_to={}", - client.utils().get_formatted_address(&to).await? - ); - println!("token_contract={token_address}"); - println!("amount_raw={amount}"); - println!("created_by_node={created_by_node}"); - println!( - "raw_transaction_matches_sdk={}", - raw_transaction.eq_ignore_ascii_case(&unsigned_raw) - ); - - let execute_raw = client - .tx() - .execute_raw_transaction(&ExecuteRawTransactionDto { - raw_transaction: raw_transaction.clone(), - signature: signature.clone(), - }) - .await?; - println!("execute_raw_result={execute_raw}"); - - let fee = client - .tx() - .calculate_transaction_fee(&CalculateTransactionFeeInput { - raw_transaction: raw_transaction.clone(), - }) - .await?; - println!("estimated_fee={:?}", fee.transaction_fee); - - if !should_send { - println!("dry_run=true"); - return Ok(()); - } - - let sent = client - .tx() - .send_raw_transaction(&SendRawTransactionInput { - transaction: raw_transaction, - signature, - return_transaction: true, - }) - .await?; - println!("transaction_id={}", sent.transaction_id); - - for _ in 0..15 { - let result = client - .tx() - .get_transaction_result(&sent.transaction_id) - .await?; - println!("status={}", result.status); - if result.status != "PENDING" { - println!("tx_fee={:?}", result.get_transaction_fees()); - println!("block_number={}", result.block_number); - if !result.error.is_empty() { - println!("error={}", result.error); - } - break; - } - sleep(Duration::from_secs(1)).await; - } - - Ok(()) -} +include!("../crates/aelf-sdk/examples/raw_transaction_flow.rs"); diff --git a/examples/token_transfer.rs b/examples/token_transfer.rs index 316b1f4..580205a 100644 --- a/examples/token_transfer.rs +++ b/examples/token_transfer.rs @@ -1,103 +1 @@ -use aelf_sdk::proto::token::TransferInput; -use aelf_sdk::{decode_address, parse_aelf_address, AElfClient, ClientConfig, Wallet}; -use prost::Message; -use tokio::time::{sleep, Duration}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let endpoint = - std::env::var("AELF_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8000".to_owned()); - let private_key = std::env::var("AELF_PRIVATE_KEY")?; - let to = std::env::var("AELF_TO_ADDRESS")?; - let amount = std::env::var("AELF_AMOUNT") - .ok() - .map(|value| value.parse::()) - .transpose()? - .unwrap_or(1); - let token_address = match std::env::var("AELF_TOKEN_CONTRACT") { - Ok(value) if !value.is_empty() => value, - _ => String::new(), - }; - let should_send = matches!( - std::env::var("AELF_SEND").ok().as_deref(), - Some("1" | "true" | "TRUE" | "yes" | "YES") - ); - let max_fee = std::env::var("AELF_MAX_FEE") - .ok() - .map(|value| value.parse::()) - .transpose()? - .unwrap_or(0.01_f64); - - let client = AElfClient::new(ClientConfig::new(endpoint))?; - let wallet = Wallet::from_private_key(&private_key)?; - let to = parse_aelf_address(&to).to_owned(); - let token_address = if token_address.is_empty() { - client - .utils() - .get_contract_address_by_name("AElf.ContractNames.Token") - .await? - } else { - token_address - }; - let formatted = client.utils().get_formatted_address(&to).await?; - let input = TransferInput { - to: Some(aelf_sdk::proto::aelf::Address { - value: decode_address(&to)?, - }), - symbol: "ELF".to_owned(), - amount, - memo: "transfer from rust sdk example".to_owned(), - }; - let transaction = client - .transaction_builder() - .with_wallet(wallet.clone()) - .with_contract(token_address) - .with_method("Transfer") - .with_message(&input) - .build_signed() - .await?; - let raw_transaction = hex::encode(transaction.encode_to_vec()); - let fee = client - .tx() - .calculate_transaction_fee(&aelf_sdk::dto::CalculateTransactionFeeInput { - raw_transaction: raw_transaction.clone(), - }) - .await?; - let elf_fee = fee.transaction_fee.get("ELF").copied().unwrap_or_default(); - - println!("from={}", wallet.address()); - println!("to={to}"); - println!("formatted_to={formatted}"); - println!("amount_raw={amount}"); - println!("estimated_fee={:?}", fee.transaction_fee); - - if !should_send { - println!("dry_run=true"); - return Ok(()); - } - - if elf_fee > max_fee { - return Err(format!("estimated ELF fee {elf_fee} exceeds max fee {max_fee}").into()); - } - - let output = client.tx().send_transaction(&raw_transaction).await?; - println!("transaction_id={}", output.transaction_id); - - for _ in 0..15 { - let result = client - .tx() - .get_transaction_result(&output.transaction_id) - .await?; - println!("status={}", result.status); - if result.status != "PENDING" { - println!("tx_fee={:?}", result.get_transaction_fees()); - if !result.error.is_empty() { - println!("error={}", result.error); - } - break; - } - sleep(Duration::from_secs(1)).await; - } - - Ok(()) -} +include!("../crates/aelf-sdk/examples/token_transfer.rs"); diff --git a/examples/wallet_keystore_roundtrip.rs b/examples/wallet_keystore_roundtrip.rs index 2411af9..8240940 100644 --- a/examples/wallet_keystore_roundtrip.rs +++ b/examples/wallet_keystore_roundtrip.rs @@ -1,12 +1 @@ -use aelf_sdk::{Keystore, Wallet}; - -fn main() -> Result<(), Box> { - let wallet = Wallet::create()?; - let keystore = Keystore::encrypt_js(&wallet, "123123")?; - let unlocked = keystore.unlock_js("123123")?; - - println!("address={}", unlocked.address); - println!("private_key={}", unlocked.private_key); - println!("mnemonic={}", unlocked.mnemonic); - Ok(()) -} +include!("../crates/aelf-sdk/examples/wallet_keystore_roundtrip.rs");