From c7f79bb16bd78b84e1609623ad9c23c3dfc3cc42 Mon Sep 17 00:00:00 2001 From: Nic Date: Tue, 21 Jul 2026 10:45:05 +0100 Subject: [PATCH] feat(ffi): production connect_default* (vendored peers) + data_dir arg on all connect paths V2-637: connect_default / connect_default_with_wallet / connect_default_for_external_signer reach the production network with zero configuration -- bootstrap peers vendored from ant-client's resources/bootstrap_peers.toml (same compiled-in pattern as antd), converted to /ip4//udp//quic multiaddrs; the wallet and external-signer variants preset the production EVM network (same coordinates as networkInfo("arbitrum-one")). V2-643: every connect constructor takes an optional trailing data_dir (uniffi default None -- existing call sites keep compiling). When set, the SDK plants HOME before core reads it, replacing the app-side libc setenv shim (AntFfiBootstrap.kt) Android apps currently need to avoid HomeDirNotFound. Bindings verified: Swift/Kotlin expose connectDefault* and dataDir with language-level defaults (String? = nil / = null). Co-Authored-By: Claude Fable 5 --- ffi/README.md | 2 +- ffi/rust/Cargo.lock | 2 + ffi/rust/ant-ffi/Cargo.toml | 4 + .../ant-ffi/resources/bootstrap_peers.toml | 17 ++ ffi/rust/ant-ffi/src/client.rs | 170 ++++++++++++++++-- 5 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 ffi/rust/ant-ffi/resources/bootstrap_peers.toml diff --git a/ffi/README.md b/ffi/README.md index 9540000..80cab2a 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -110,7 +110,7 @@ renders them camelCase in Swift/Kotlin, e.g. `chunk_put` → `chunkPut`): | Domain | Methods | |---|---| -| **Connect** | `connect_local`, `connect`, `connect_with_wallet`, `connect_from_devnet_manifest`, `connect_from_devnet_manifest_external_signer`, `connect_for_external_signer` | +| **Connect** | `connect_default`, `connect_default_with_wallet`, `connect_default_for_external_signer` (production network, vendored peers), `connect_local`, `connect`, `connect_with_wallet`, `connect_from_devnet_manifest`, `connect_from_devnet_manifest_external_signer`, `connect_for_external_signer` — all take an optional `data_dir` (required on Android: pass the app's files dir) | | **Chunks** | `chunk_put`, `chunk_get`, `chunk_exists` | | **Data (bytes)** | `data_put_public`, `data_get_public`, `data_put_private`, `data_get_private` | | **Files** | `file_upload_public`, `file_upload_private`, `file_download_public`, `download_public_to_file`, `download_private_to_file`, `estimate_file_cost` | diff --git a/ffi/rust/Cargo.lock b/ffi/rust/Cargo.lock index 1fba6e8..5237d23 100644 --- a/ffi/rust/Cargo.lock +++ b/ffi/rust/Cargo.lock @@ -855,10 +855,12 @@ dependencies = [ "hex", "reqwest 0.12.28", "rmp-serde", + "serde", "serde_json", "thiserror 2.0.18", "tiny-keccak", "tokio", + "toml 0.8.23", "uniffi", "url", "zeroize", diff --git a/ffi/rust/ant-ffi/Cargo.toml b/ffi/rust/ant-ffi/Cargo.toml index c0e4b89..f874f54 100644 --- a/ffi/rust/ant-ffi/Cargo.toml +++ b/ffi/rust/ant-ffi/Cargo.toml @@ -37,8 +37,12 @@ hex = "0.4" # a system OpenSSL. Already in the dep graph via evmlib. reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } rmp-serde = "1" +serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" +# Parses the vendored resources/bootstrap_peers.toml (same format + crate as +# antd's compiled-in fallback copy). +toml = "0.8" url = "2" zeroize = "1" tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } diff --git a/ffi/rust/ant-ffi/resources/bootstrap_peers.toml b/ffi/rust/ant-ffi/resources/bootstrap_peers.toml new file mode 100644 index 0000000..ce77014 --- /dev/null +++ b/ffi/rust/ant-ffi/resources/bootstrap_peers.toml @@ -0,0 +1,17 @@ +# Autonomi Network — Bootstrap Peers +# +# This file provides initial peers for joining the production network. +# It is loaded automatically when no --bootstrap CLI argument is provided. +# +# Format: "ip:port" socket addresses. +# Port range for ant-node: 10000-10999. + +peers = [ + "207.148.94.42:10000", + "45.77.50.10:10000", + "66.135.23.83:10000", + "149.248.9.2:10000", + "49.12.119.240:10000", + "5.161.25.133:10000", + "18.228.202.183:10000", +] diff --git a/ffi/rust/ant-ffi/src/client.rs b/ffi/rust/ant-ffi/src/client.rs index df4eb84..d7928a8 100644 --- a/ffi/rust/ant-ffi/src/client.rs +++ b/ffi/rust/ant-ffi/src/client.rs @@ -24,6 +24,59 @@ use crate::{ TxRequest, }; +/// Mainnet bootstrap peers vendored from ant-client's +/// `resources/bootstrap_peers.toml` at the pinned ant-core tag, so the +/// `connect_default*` constructors can reach the production network with zero +/// configuration — the same last-resort pattern as antd's compiled-in copy. +/// Re-copy from the pinned checkout whenever the ant-core pin is bumped. +const COMPILED_IN_BOOTSTRAP_PEERS_TOML: &str = include_str!("../resources/bootstrap_peers.toml"); + +#[derive(serde::Deserialize)] +struct BootstrapConfig { + peers: Vec, +} + +/// Parse the vendored peer list ("ip:port" socket addresses) into the +/// `/ip4//udp//quic` multiaddr strings [`Client::connect`] expects. +/// Errors instead of silently returning an empty list — a malformed vendored +/// file is a build-time regression, not a runtime condition. +fn default_bootstrap_peer_strings() -> Result, ClientError> { + let cfg: BootstrapConfig = toml::from_str(COMPILED_IN_BOOTSTRAP_PEERS_TOML).map_err(|e| { + ClientError::InternalError { + reason: format!("vendored bootstrap_peers.toml is malformed: {e}"), + } + })?; + let peers: Vec = cfg + .peers + .iter() + .filter_map(|s| s.parse::().ok()) + .map(|sa| { + let ip_tag = if sa.is_ipv4() { "ip4" } else { "ip6" }; + format!("/{}/{}/udp/{}/quic", ip_tag, sa.ip(), sa.port()) + }) + .collect(); + if peers.is_empty() { + return Err(ClientError::InternalError { + reason: "vendored bootstrap_peers.toml contains no usable peers".into(), + }); + } + Ok(peers) +} + +/// Plant the directory Autonomi derives its local state paths from (bootstrap +/// cache, config). At the pinned ant-core those paths come from the `HOME` +/// env var; Android app processes have no `HOME`, so `connect*` fails with +/// `HomeDirNotFound` unless one is planted — previously done app-side via a +/// libc `setenv` shim (the demos' `AntFfiBootstrap.kt`). Passing `data_dir` +/// does the planting SDK-side. No-op when `None`. +fn apply_data_dir(data_dir: Option<&str>) { + if let Some(dir) = data_dir { + // Must run before any core call that reads HOME (`P2PNode::new` reads + // the saorsa bootstrap cache under it). + std::env::set_var("HOME", dir); + } +} + /// Map an ant-core [`UploadEvent`] to the FFI [`ProgressUpdate`] shape. fn map_upload_event(ev: UploadEvent) -> ProgressUpdate { match ev { @@ -163,8 +216,11 @@ impl Client { #[uniffi::export(async_runtime = "tokio")] impl Client { /// Connect to a local test network. - #[uniffi::constructor] - pub async fn connect_local() -> Result, ClientError> { + /// + /// `data_dir`: see [`Self::connect`]. + #[uniffi::constructor(default(data_dir = None))] + pub async fn connect_local(data_dir: Option) -> Result, ClientError> { + apply_data_dir(data_dir.as_deref()); let builder = CoreNodeConfig::builder() .mode(NodeMode::Client) .port(0) @@ -196,9 +252,21 @@ impl Client { Ok(Self::wrap(client)) } - /// Connect to the network using explicit bootstrap peers. - #[uniffi::constructor] - pub async fn connect(peers: Vec) -> Result, ClientError> { + /// Connect to the network using explicit bootstrap peers + /// (`/ip4//udp//quic` multiaddr strings). + /// + /// `data_dir` overrides the directory Autonomi's local state (bootstrap + /// cache, config) lives under. **Required on Android** — pass the app's + /// files directory (`context.filesDir`); Android processes have no + /// `HOME`, so connecting without it fails with `InitializationFailed` + /// (`HomeDirNotFound`). Leave `None` on iOS / desktop to use the + /// platform default. + #[uniffi::constructor(default(data_dir = None))] + pub async fn connect( + peers: Vec, + data_dir: Option, + ) -> Result, ClientError> { + apply_data_dir(data_dir.as_deref()); let mut builder = CoreNodeConfig::builder() .mode(NodeMode::Client) .port(0) @@ -245,18 +313,76 @@ impl Client { Ok(Self::wrap(client)) } + /// Connect to the Autonomi **production network** using the bootstrap + /// peers vendored into the SDK — no configuration needed. Read-only + /// client; for uploads use [`Self::connect_default_with_wallet`] or + /// [`Self::connect_default_for_external_signer`]. + /// + /// `data_dir`: see [`Self::connect`]. + #[uniffi::constructor(default(data_dir = None))] + pub async fn connect_default(data_dir: Option) -> Result, ClientError> { + Self::connect(default_bootstrap_peer_strings()?, data_dir).await + } + + /// [`Self::connect_default`] with a wallet attached for write operations, + /// preset for the production EVM network (same coordinates as + /// `networkInfo("arbitrum-one")`). + /// + /// `data_dir`: see [`Self::connect`]. + #[uniffi::constructor(default(data_dir = None))] + pub async fn connect_default_with_wallet( + private_key: String, + data_dir: Option, + ) -> Result, ClientError> { + let net = crate::payments::network_info("arbitrum-one".into())?; + Self::connect_with_wallet( + default_bootstrap_peer_strings()?, + private_key, + net.rpc_url, + net.token_address, + net.vault_address, + data_dir, + ) + .await + } + + /// [`Self::connect_default`] configured for the **external-signer** flow + /// (mobile wallets / WalletConnect): production peers + production EVM + /// network for quotes, no wallet attached. Pay via `prepare_*` + your + /// signer + `finalize_*`. + /// + /// `data_dir`: see [`Self::connect`]. + #[uniffi::constructor(default(data_dir = None))] + pub async fn connect_default_for_external_signer( + data_dir: Option, + ) -> Result, ClientError> { + let net = crate::payments::network_info("arbitrum-one".into())?; + Self::connect_for_external_signer( + default_bootstrap_peer_strings()?, + net.rpc_url, + net.token_address, + net.vault_address, + data_dir, + ) + .await + } + /// Connect to the network with a wallet configured for write operations. /// /// Takes the wallet private key and EVM network details directly, /// since the wallet must be constructed fresh for ownership transfer. - #[uniffi::constructor] + /// + /// `data_dir`: see [`Self::connect`]. + #[uniffi::constructor(default(data_dir = None))] pub async fn connect_with_wallet( peers: Vec, mut private_key: String, rpc_url: String, payment_token_address: String, payment_vault_address: String, + data_dir: Option, ) -> Result, ClientError> { + apply_data_dir(data_dir.as_deref()); let mut builder = CoreNodeConfig::builder() .mode(NodeMode::Client) .port(0) @@ -326,8 +452,14 @@ impl Client { /// /// Fails if the manifest doesn't exist, is malformed, or has no `evm` /// section (a devnet started without payment enforcement). - #[uniffi::constructor] - pub async fn connect_from_devnet_manifest(path: String) -> Result, ClientError> { + /// + /// `data_dir`: see [`Self::connect`]. + #[uniffi::constructor(default(data_dir = None))] + pub async fn connect_from_devnet_manifest( + path: String, + data_dir: Option, + ) -> Result, ClientError> { + apply_data_dir(data_dir.as_deref()); let bytes = std::fs::read(&path).map_err(|e| ClientError::InitializationFailed { reason: format!("failed to read manifest at {path}: {e}"), })?; @@ -415,10 +547,14 @@ impl Client { /// attaches **no wallet** (the manifest's `wallet_private_key` may be empty /// — e.g. the Sepolia devnet, which expects you to bring your own wallet). /// Pay via `prepare_*` + an external signer + `finalize_upload`. - #[uniffi::constructor] + /// + /// `data_dir`: see [`Self::connect`]. + #[uniffi::constructor(default(data_dir = None))] pub async fn connect_from_devnet_manifest_external_signer( path: String, + data_dir: Option, ) -> Result, ClientError> { + apply_data_dir(data_dir.as_deref()); let bytes = std::fs::read(&path).map_err(|e| ClientError::InitializationFailed { reason: format!("failed to read manifest at {path}: {e}"), })?; @@ -827,13 +963,17 @@ impl Client { /// queries work (they need the network), but payment is signed off-device /// by an external wallet (e.g. WalletConnect). Use [`Self::prepare_data_upload`] /// / [`Self::prepare_file_upload`] then [`Self::finalize_upload`]. - #[uniffi::constructor] + /// + /// `data_dir`: see [`Self::connect`]. + #[uniffi::constructor(default(data_dir = None))] pub async fn connect_for_external_signer( peers: Vec, rpc_url: String, payment_token_address: String, payment_vault_address: String, + data_dir: Option, ) -> Result, ClientError> { + apply_data_dir(data_dir.as_deref()); let mut builder = CoreNodeConfig::builder() .mode(NodeMode::Client) .port(0) @@ -1365,6 +1505,16 @@ fn hex_to_address(hex: &str) -> Result<[u8; 32], ClientError> { mod tests { use super::*; + #[test] + fn default_bootstrap_peers_are_valid_multiaddrs() { + let peers = default_bootstrap_peer_strings().unwrap(); + assert_eq!(peers.len(), 7, "vendored mainnet peer count"); + for p in &peers { + assert!(p.starts_with("/ip4/") && p.ends_with("/quic"), "shape: {p}"); + assert!(p.parse::().is_ok(), "unparseable multiaddr: {p}"); + } + } + #[test] fn decode_hash_accepts_32_bytes_with_or_without_0x() { let bare = "11".repeat(32);