diff --git a/AGENTS.md b/AGENTS.md index 79d0431a4c..7201ff35ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -207,6 +207,14 @@ submodules consumed by `path` (not published to crates.io, so no `[patch.crates-io]` entry — same shape as `tinyhumans-sdk`). After cloning: `git submodule update --init vendor/tinydocs vendor/tinywallet`. +**What this binary takes from each repository is its `-bus` contract crate, not +its root crate.** A `-bus` crate is transport-free and holds the interface name, +the object path, one constant per member, the payload types and the contract +version — plus the pure rules a host genuinely runs itself. The root crate holds +the implementation the TinyBus module carries, and this binary does not link it. +`vendor/tinywallet/crates/tinywallet-bus` is the entry here; the same shape as +`tinyvoice-bus`, `tinyjuice-bus`, `tinyruntime-bus` and `tinydocs-bus`. + The split follows one rule, and it is worth stating because it decides where the *next* extraction goes: **a crate owns what is the same for every host; the host owns what depends on its own runtime, config, or threat model.** Both @@ -215,7 +223,7 @@ crates are therefore synchronous, I/O-free, and runtime-free. | Crate | Owns | OpenHuman keeps | | --- | --- | --- | | `tinydocs` | the `.docx` spec types, their size limits, validation, and OOXML synthesis (`docx-rs` sits behind it) | the artifact pipeline, the `spawn_blocking` hop, and the generation deadline — `src/openhuman/tools/impl/document/` | -| `tinywallet` | the BTC / EVM / Solana / Tron address formats: parsing, validation, encoding conversions | RPC endpoint resolution, transaction assembly and broadcast, key custody — `src/openhuman/web3/` | +| `tinywallet-bus` | the TinyWallet wire contract and bus member names, the BTC / EVM / Solana / Tron address formats, the EIP-712 and ERC-20 encoders, and the Tron verification codec | RPC endpoint resolution, transaction assembly and broadcast, key custody — `src/openhuman/web3/` | Consequences worth knowing before touching either seam: @@ -232,7 +240,7 @@ Consequences worth knowing before touching either seam: `tinydocs`' `DocumentSpec` re-exported under its historical name, with field names unchanged; `the_json_wire_shape_is_unchanged_by_the_extraction` pins that. -- **`tinywallet` rejects an uppercase `0X` EVM prefix, matching the code it +- **`tinywallet-bus` rejects an uppercase `0X` EVM prefix, matching the code it replaced, which rejected that prefix too.** The old path went through `ethers_core::types::Address`'s `FromStr`, which is `fixed-hash`'s and strips only a lowercase `0x` (`fixed-hash-0.8.0/src/hash.rs`, `input.strip_prefix("0x")`), so `0X…` failed @@ -241,11 +249,31 @@ Consequences worth knowing before touching either seam: - **Bitcoin has two rules, not one.** `btc::validate` is the recipient rule; `btc::validate_sender` additionally requires P2WPKH. Using the first where the second belongs accepts an address that only fails later, at signing time. -- **Each crate's gates ride OpenHuman's existing ones**: `tinydocs` is - exclusive to `documents`, `tinywallet` to `web3`. Both are default-ON and - already forwarded to the desktop shell. Note `tinydocs` is now taken with - `default-features = false` — the wire contract, not the writers, which run in - the TinyBus module instead (see the module host section). +- **`tinywallet-bus` holds logic, not only types, and that is deliberate.** Four + rules are the host's to run synchronously: validating an address before a spec + is sent (a rejected input rather than a failed call), hashing EIP-712 typed + data for the x402 payment path, encoding ERC-20 calldata, and verifying the + txid and contents of what a Tron node handed back. That last one is not + optional — Tron has the *node* build the transaction, so the check has to + happen wherever the decision to sign is made. Same precedent `tinydocs`' + spec validators set: a bus crate carrying host-side rules is established here, + not a novelty. +- **Member names come from `tinywallet_bus::names::methods`, never a literal.** + `src/openhuman/modules/wallet.rs` calls by constant, and + `wallet_tests.rs`'s `contract` module pins `registry.rs`'s `bus_name` / + `object_path` against `BUS_NAME` / `OBJECT_PATH` and every member it sends + against `METHODS` + `CONFIDENTIAL_METHODS`. The registry is a compiled-in + `const` table that cannot name a gated crate, so a drifted string is a + `NameHasNoOwner` in the field rather than a compile error. +- **The root `tinywallet` crate survives as a dev-dependency only.** Test + fixtures derive a known account through its `key` gate. Cargo does not link + dev-dependency features into the shipped binary, so this does not put + `bitcoin`, `coins-bip39` or a native `secp256k1` build back into the product. +- **Each crate's gates ride OpenHuman's existing ones**: the tinydocs entry is + exclusive to `documents`, `tinywallet-bus` to `web3`. Both are default-ON and + already forwarded to the desktop shell. Both are taken with + `default-features = false` — the wire contract, not the implementation, which + runs in the TinyBus module instead (see the module host section). ### Backend API access — `src/api/` over `tinyhumans-sdk` diff --git a/Cargo.lock b/Cargo.lock index f409adc6e1..9b1b94b0a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4193,6 +4193,7 @@ dependencies = [ "tinyplace", "tinyruntime-bus", "tinywallet", + "tinywallet-bus", "tokio", "tokio-stream", "tokio-tungstenite 0.29.0", @@ -6722,23 +6723,34 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tinywallet" -version = "0.4.0" +version = "0.5.0" dependencies = [ - "async-trait", - "bech32 0.11.1", - "bs58", "coins-bip32 0.8.7", "coins-bip39 0.13.1", "ed25519-dalek", - "hex", "hmac 0.13.0", "ripemd 0.2.0", + "sha2 0.11.0", + "sha3", + "thiserror 2.0.18", + "tinywallet-bus", + "zeroize", +] + +[[package]] +name = "tinywallet-bus" +version = "0.5.0" +dependencies = [ + "async-trait", + "bech32 0.11.1", + "bs58", + "hex", + "ripemd 0.2.0", "serde", "serde_json", "sha2 0.11.0", "sha3", "thiserror 2.0.18", - "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index be8a2607d6..06703e0bbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -523,24 +523,26 @@ tinyhosts = { path = "vendor/tinyhosts", default-features = false, features = [" # published to crates.io, so there is no `[patch.crates-io]` entry for it. # After cloning: `git submodule update --init vendor/tinywallet`. # -# Optional: exclusive to the default-ON `web3` feature. -# Taken WITHOUT `tx` or `client`, which is the whole point: those are the only -# gates that pull the `bitcoin` crate and its native secp256k1 build, and -# transaction building now happens in the loaded `tinywallet` module instead. -# What stays is address validation, key derivation, the `Transport` seam, the -# wire contract, EIP-712 hashing and ERC-20 calldata — none of which needs a -# chain library. -# Key derivation (`key`) and transaction signing (`tx`) are deliberately absent: -# both happen inside the loaded tinywallet module now, so this binary links -# neither. What is left is the wire contract, address validation, the EIP-712 / -# ABI encoders the x402 payment path builds with, the `Transport` seam, and -# `tx-codec` — the Tron *verifier*, which runs host-side before a transaction is -# sent for signing and needs no `bitcoin` build. -# -# `key` is re-enabled under [dev-dependencies] so test fixtures can still derive -# a known account. Dev-dependency features are not linked into the shipped -# binary, so that does not undo the shed. -tinywallet = { path = "vendor/tinywallet", default-features = false, features = ["btc", "evm", "solana", "tron", "keccak", "net", "wire", "eip712", "abi", "tx-codec"], optional = true } +# Optional: exclusive to the `web3` feature — OFF in the contributor set +# (`[features] default`) and ON in the product set +# (`scripts/ci/product-features.txt`), so it ships but a bare `cargo check` does +# not pay for it. See the two-set note above `default`. +# +# Taken as `tinywallet-bus`, the contract crate, and NOT the root `tinywallet` +# crate. The root crate is where key derivation, transaction building, signing +# and the chain clients live, and those are the gates that pull the `bitcoin` +# crate and its native secp256k1 build; all of it happens inside the loaded +# `tinywallet` module now, so this binary links none of it. What the contract +# crate carries is exactly what a host still runs itself: the wire types that +# cross the bus, the bus member names, address validation, the EIP-712 and ERC-20 +# encoders the x402 payment path builds with, the `Transport` seam this crate +# implements, and the Tron verifier that checks what a node handed back before +# a transaction is sent for signing. +# +# The root crate is still taken under [dev-dependencies], where test fixtures +# derive a known account. Dev-dependency features are not linked into the +# shipped binary, so that does not undo the shed. +tinywallet-bus = { path = "vendor/tinywallet/crates/tinywallet-bus", default-features = false, features = ["btc", "evm", "solana", "tron", "keccak", "net", "wire", "eip712", "abi", "tx-codec"], optional = true } # secp256k1 signing over the digests the wallet module hands back. Pure Rust, # and already in the graph beneath `coins-bip32` (which derives the key being @@ -848,7 +850,7 @@ voice = [ # to derive a Solana ATA, which is address arithmetic rather than signing, and # tinyplace pulls it in through `ed25519-dalek` regardless. web3 = [ - "dep:tinywallet", + "dep:tinywallet-bus", "dep:curve25519-dalek", "modules", ] diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index b232d5e75a..37af4c2250 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -4430,7 +4430,7 @@ dependencies = [ "tinymemory-tinycortex", "tinyplace", "tinyruntime-bus", - "tinywallet", + "tinywallet-bus", "tokio", "tokio-stream", "tokio-tungstenite 0.29.0", @@ -7432,8 +7432,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "tinywallet" -version = "0.4.0" +name = "tinywallet-bus" +version = "0.5.0" dependencies = [ "async-trait", "bech32", diff --git a/src/openhuman/modules/registry.rs b/src/openhuman/modules/registry.rs index 28ac510513..7db56d9483 100644 --- a/src/openhuman/modules/registry.rs +++ b/src/openhuman/modules/registry.rs @@ -105,8 +105,10 @@ const TINYDOCS: ModuleRecord = ModuleRecord { /// /// **This host sends it the recovery phrase, over confidential calls, and never /// derives or signs itself.** All four chains — Bitcoin, EVM, Solana and Tron — -/// derive and sign inside the module. This binary links neither `tinywallet`'s -/// `key` feature nor `k256`; see the note on the `tinywallet` dependency. +/// derive and sign inside the module. This binary does not link the root +/// `tinywallet` crate at all — it takes `tinywallet-bus`, the wire contract, +/// which carries no `key` gate — nor does it link `k256`; see the note on the +/// `tinywallet-bus` dependency. /// /// The phrase is only sent to a module tinybus has attested *and* whose digest /// matches one of the entries below — `super::wallet::attested_proxy` checks @@ -131,63 +133,63 @@ const TINYWALLET: ModuleRecord = ModuleRecord { description: "Transaction building and assembly for Bitcoin, EVM, Solana and Tron", bus_name: "ai.tinyhumans.tinywallet.Wallet", object_path: "/ai/tinyhumans/tinywallet/Wallet", - version: "0.4.0", - release_url: "https://github.com/tinyhumansai/tinywallet/releases/tag/v0.4.0", + version: "0.5.0", + release_url: "https://github.com/tinyhumansai/tinywallet/releases/tag/v0.5.0", assets: &[ PlatformAsset { host_key: "ubuntu-24.04-x86_64", - archive: "tinywallet-module-0.4.0-ubuntu-24.04-x86_64.tar.gz", - sha256: "737a18c258bb9013ad85006433c72a5dc83b94de8f15a0d37723a3b96cf047fa", + archive: "tinywallet-module-0.5.0-ubuntu-24.04-x86_64.tar.gz", + sha256: "03906b3e2bb6f24a230e29eefc916299d0e9269c166c8766c12769545fbe602d", }, PlatformAsset { host_key: "ubuntu-24.04-arm64", - archive: "tinywallet-module-0.4.0-ubuntu-24.04-arm64.tar.gz", - sha256: "72217d4f4dc1a2328de08c83d24998cd51729e8157cd2e9cb3b034ec1da2ea94", + archive: "tinywallet-module-0.5.0-ubuntu-24.04-arm64.tar.gz", + sha256: "8630d4d3bd49047606b19b53cc1c16eaf114ee12a7693ce14882c395cd6141de", }, PlatformAsset { host_key: "ubuntu-22.04-x86_64", - archive: "tinywallet-module-0.4.0-ubuntu-22.04-x86_64.tar.gz", - sha256: "e7d2d1a40331b5fea1dc9d8870c206d093c756af91790a15e3fcc9fc1b160158", + archive: "tinywallet-module-0.5.0-ubuntu-22.04-x86_64.tar.gz", + sha256: "a680eb8e52caa6e367c914f0c08505569022362bff3b7bcd8ca79f931fcc12bf", }, PlatformAsset { host_key: "ubuntu-22.04-arm64", - archive: "tinywallet-module-0.4.0-ubuntu-22.04-arm64.tar.gz", - sha256: "248fd13ba59ab9c00ccd605b60c533aabd41be0f82cd167758524842122510f1", + archive: "tinywallet-module-0.5.0-ubuntu-22.04-arm64.tar.gz", + sha256: "7e38bde187aba01cacac78c86fa2b29526f8feabb21c4992a1d240763f509aea", }, PlatformAsset { host_key: "macos-26-arm64", - archive: "tinywallet-module-0.4.0-macos-26-arm64.tar.gz", - sha256: "e6df7dc830d595a63af6864cbec6e3e22e51f35af558e7b62fa655d6b16d0581", + archive: "tinywallet-module-0.5.0-macos-26-arm64.tar.gz", + sha256: "40ff703a3f609db1b40083e1f03f3291a88a838b931602ebd19116c8dbaedf64", }, PlatformAsset { host_key: "macos-26-x86_64", - archive: "tinywallet-module-0.4.0-macos-26-x86_64.tar.gz", - sha256: "fd197ac908057b9b5b4c7aef1b86e74ea7369133eff2a4835c310c73e7816a01", + archive: "tinywallet-module-0.5.0-macos-26-x86_64.tar.gz", + sha256: "1d6a035bcf5a94591023536b974a5b594acec6c83939b2505730efe8cf1ae530", }, PlatformAsset { host_key: "macos-15-arm64", - archive: "tinywallet-module-0.4.0-macos-15-arm64.tar.gz", - sha256: "28a56ed94827b46a972c054b07e614684b7217d8f8c69373e93b957de336901b", + archive: "tinywallet-module-0.5.0-macos-15-arm64.tar.gz", + sha256: "3a56c28c29a4c9047be3fd730c28e7a8b07e27cd3655b5c2ff2832e762d2bf1a", }, PlatformAsset { host_key: "macos-15-x86_64", - archive: "tinywallet-module-0.4.0-macos-15-x86_64.tar.gz", - sha256: "2e97717f08efefb90a8be51f389cbf826fb132fc7111f11837e9ee717c527e58", + archive: "tinywallet-module-0.5.0-macos-15-x86_64.tar.gz", + sha256: "77e99f160f435cbf227d91a41738849d1d26f3ce0b607c73d32e505c54e5aa84", }, PlatformAsset { host_key: "windows-2025-x86_64", - archive: "tinywallet-module-0.4.0-windows-2025-x86_64.zip", - sha256: "c9393d6c0f171db34298950ad029c21ea6b41f3f77971cf6668ebbd7f34736b7", + archive: "tinywallet-module-0.5.0-windows-2025-x86_64.zip", + sha256: "9e677b63f3371728cf783cd7439f680d837e654e40eb42d6b1f12ec8dce7965a", }, PlatformAsset { host_key: "windows-2022-x86_64", - archive: "tinywallet-module-0.4.0-windows-2022-x86_64.zip", - sha256: "8ed5e86977f951a8c54dbde82914f6f936d4402564beb30406f4140d4be02872", + archive: "tinywallet-module-0.5.0-windows-2022-x86_64.zip", + sha256: "4fc049696ef9897a3aada0f4322b70b98d561b689bb51f90ebe30a9916f47fba", }, PlatformAsset { host_key: "windows-11-arm64", - archive: "tinywallet-module-0.4.0-windows-11-arm64.zip", - sha256: "7854dfeb1dd04afe99488616e223a0f3ce6d7c671e22f8fbc3089eb0523cbf51", + archive: "tinywallet-module-0.5.0-windows-11-arm64.zip", + sha256: "d22513e74c435ac541b1827c17c598c87df00fc2526e75afbdd18bdaf71c002b", }, ], load: LoadPolicy::Lazy, diff --git a/src/openhuman/modules/wallet.rs b/src/openhuman/modules/wallet.rs index 1dcfb6e80d..d45f08988e 100644 --- a/src/openhuman/modules/wallet.rs +++ b/src/openhuman/modules/wallet.rs @@ -66,7 +66,8 @@ //! with which it is, and [`sign_payload`] dispatches on the tag rather than on //! the chain — so a chain that changes scheme cannot silently sign wrongly. -use tinywallet::wire::{ +use tinywallet_bus::names::methods; +use tinywallet_bus::wire::{ DerivedAccount, ExportRequest, ExportedKey, Scheme, SecretMaterial, SignMessageRequest, SignRequest, Signature, SignedTransaction, TransactionSpec, }; @@ -125,7 +126,7 @@ pub async fn sign_transaction_in_module( ); proxy .call_confidential( - "SignTransaction", + methods::SIGN_TRANSACTION, (SignRequest { secret: secret.clone(), transaction: transaction.clone(), @@ -150,7 +151,7 @@ pub async fn derive_account( secret.chain ); proxy - .call_confidential("DeriveAccount", (secret.clone(),)) + .call_confidential(methods::DERIVE_ACCOUNT, (secret.clone(),)) .await .map_err(|error| classify(&error)) } @@ -181,7 +182,7 @@ pub async fn sign_message( ); proxy .call_confidential( - "SignMessage", + methods::SIGN_MESSAGE, (SignMessageRequest { secret: secret.clone(), message_hex: hex(message), @@ -213,7 +214,7 @@ pub async fn export_key( ); proxy .call_confidential( - "ExportKey", + methods::EXPORT_KEY, (ExportRequest { secret: secret.clone(), },), diff --git a/src/openhuman/modules/wallet_tests.rs b/src/openhuman/modules/wallet_tests.rs index f018d0ffe1..e1632ba1fc 100644 --- a/src/openhuman/modules/wallet_tests.rs +++ b/src/openhuman/modules/wallet_tests.rs @@ -7,8 +7,8 @@ //! themselves are covered where they can be honest: `tinywallet`'s own loader //! E2E, which drives a real module over a real broker. -use tinywallet::wire::{Scheme, Signature, SigningPayload, TransactionSpec}; -use tinywallet::Chain; +use tinywallet_bus::wire::TransactionSpec; +use tinywallet_bus::Chain; use super::{classify, WalletCallError}; use crate::openhuman::config::Config; @@ -37,8 +37,8 @@ fn failure(name: &str) -> tinybus::Error { /// /// No key is derived here any more: the module does that. This is the request, /// not the secret it produces. -fn evm_signing_secret() -> tinywallet::wire::SecretMaterial { - tinywallet::wire::SecretMaterial { +fn evm_signing_secret() -> tinywallet_bus::wire::SecretMaterial { + tinywallet_bus::wire::SecretMaterial { mnemonic: VECTOR.to_string(), derivation_path: "m/44'/60'/0'/0/0".to_string(), chain: Chain::Evm, @@ -196,7 +196,7 @@ mod attestation_guard { /// These assert the two shapes are genuinely distinct, so the wrapper cannot be /// dropped again without a test failing. mod request_shapes { - use tinywallet::wire::{ExportRequest, SecretMaterial, SignMessageRequest, SignRequest}; + use tinywallet_bus::wire::{ExportRequest, SecretMaterial, SignMessageRequest, SignRequest}; fn secret() -> SecretMaterial { super::evm_signing_secret() @@ -223,14 +223,14 @@ mod request_shapes { let sign_message = serde_json::to_value(SignMessageRequest { secret: secret(), message_hex: "00".repeat(32), - scheme: tinywallet::wire::Scheme::Secp256k1Prehash, + scheme: tinywallet_bus::wire::Scheme::Secp256k1Prehash, }) .unwrap(); assert!(serde_json::from_value::(sign_message).is_err()); assert!(serde_json::from_value::( serde_json::to_value(SignRequest { secret: secret(), - transaction: tinywallet::wire::TransactionSpec::Evm { + transaction: tinywallet_bus::wire::TransactionSpec::Evm { to: format!("0x{}", "11".repeat(20)), value_wei: "1".to_string(), data_hex: "0x".to_string(), @@ -245,3 +245,61 @@ mod request_shapes { .is_err()); } } + +// --------------------------------------------------------------------------- +// The contract this client compiles against +// --------------------------------------------------------------------------- + +/// `registry.rs` is a compiled-in `const` table, and it cannot name a gated +/// crate: its `bus_name` and `object_path` are string literals sitting next to +/// the module's own spelling of them with nothing between the two. A mismatch is +/// therefore not a compile error — it is a `NameHasNoOwner` at first use, in the +/// field, on whichever platform nobody tested. These two tests are what stands +/// in for the compiler. +mod contract { + use crate::openhuman::modules::registry; + + #[test] + fn the_registry_entry_matches_the_interface_this_client_calls() { + let record = registry::find("tinywallet").expect("the tinywallet record is compiled in"); + assert_eq!(record.bus_name, tinywallet_bus::BUS_NAME); + assert_eq!(record.object_path, tinywallet_bus::OBJECT_PATH); + assert!( + record.object_path.starts_with('/') && !record.object_path.contains('.'), + "an object path with a dot in it is rejected by the loader, not by the compiler" + ); + } + + #[test] + fn every_member_this_client_calls_is_one_the_contract_declares() { + // The direction that matters. A name this host sends that the module + // does not serve fails at call time with nothing to catch it earlier; + // the reverse — a member the module serves and this host never calls — + // is fine, and two of them are exactly that. `BuildUnsigned` and + // `AttachSignature` are the two-round-trip flow for a backend that + // cannot be trusted with a key, which does not apply to a module whose + // artifact this build hashed, so the confidential members are the ones + // used here. + use tinywallet_bus::names::methods; + + let called = [ + methods::SIGN_TRANSACTION, + methods::DERIVE_ACCOUNT, + methods::SIGN_MESSAGE, + methods::EXPORT_KEY, + ]; + for member in called { + assert!( + tinywallet_bus::METHODS.contains(&member), + "{member} is not a member of {}", + tinywallet_bus::BUS_NAME + ); + // And every one of them carries a recovery phrase, so every one has + // to go out over a confidential call rather than a plain one. + assert!( + tinywallet_bus::CONFIDENTIAL_METHODS.contains(&member), + "{member} is called confidentially but not declared confidential" + ); + } + } +} diff --git a/src/openhuman/web3/wallet/abi.rs b/src/openhuman/web3/wallet/abi.rs index 10a8c3577e..8750482eb3 100644 --- a/src/openhuman/web3/wallet/abi.rs +++ b/src/openhuman/web3/wallet/abi.rs @@ -1,11 +1,11 @@ -//! ERC-20 calldata, delegated to `tinywallet`. +//! ERC-20 calldata, delegated to `tinywallet-bus`. //! //! This used to hand-build an `ethers_core::abi::Function` to encode one call. //! That worked, and it cost the whole `ethers-core` ABI machinery — a type //! grammar, a bignum, and their tails — to produce a four-byte selector //! followed by two 32-byte words. //! -//! `tinywallet::abi` owns that encoding now, over `sha3` alone, and +//! `tinywallet_bus::abi` owns that encoding now, over `sha3` alone, and //! deliberately sits outside its `tx` gate: calldata is an *input* to building //! a transaction, so a host that builds elsewhere still needs it locally rather //! than paying a bus round trip for keccak over 68 bytes. @@ -27,18 +27,20 @@ /// amount is not a non-negative integer that fits in 256 bits. #[allow(unreachable_patterns)] pub fn encode_erc20_transfer(to_address: &str, amount_raw: &str) -> Result { - tinywallet::abi::encode_erc20_transfer(to_address, amount_raw).map_err(|error| match error { - tinywallet::abi::Error::InvalidRecipient { .. } => { - format!("invalid EVM recipient address '{to_address}': {error}") - } - // Preserves the wording the previous implementation used, because the - // agent tool's schema documents it and a model reads it to correct - // itself. - tinywallet::abi::Error::InvalidAmount { .. } => { - format!("amount '{amount_raw}' is not a valid non-negative integer") - } - _ => error.to_string(), - }) + tinywallet_bus::abi::encode_erc20_transfer(to_address, amount_raw).map_err( + |error| match error { + tinywallet_bus::abi::Error::InvalidRecipient { .. } => { + format!("invalid EVM recipient address '{to_address}': {error}") + } + // Preserves the wording the previous implementation used, because the + // agent tool's schema documents it and a model reads it to correct + // itself. + tinywallet_bus::abi::Error::InvalidAmount { .. } => { + format!("amount '{amount_raw}' is not a valid non-negative integer") + } + _ => error.to_string(), + }, + ) } #[cfg(test)] diff --git a/src/openhuman/web3/wallet/chains/btc.rs b/src/openhuman/web3/wallet/chains/btc.rs index 31662c0a16..3230dcf675 100644 --- a/src/openhuman/web3/wallet/chains/btc.rs +++ b/src/openhuman/web3/wallet/chains/btc.rs @@ -56,12 +56,12 @@ pub fn estimated_btc_fee_sats() -> u64 { /// Used for recipients (we don't care what address type they prefer; the /// `bitcoin` crate's script_pubkey() will encode P2WPKH/P2TR/P2SH correctly). /// -/// Delegates to the vendored [`tinywallet`] crate, which owns the address +/// Delegates to the vendored [`tinywallet_bus`] crate, which owns the address /// format itself. Nothing about parsing a Bitcoin address is OpenHuman- /// specific, so the rules live where any host can reach them; what stays here /// is the `Result<_, String>` shape the rest of this domain speaks. pub fn validate_btc_address(addr: &str) -> Result { - let result = tinywallet::address::btc::validate(addr).map_err(|e| e.to_string()); + let result = tinywallet_bus::address::btc::validate(addr).map_err(|e| e.to_string()); debug!( "{LOG_PREFIX} validate_address role=recipient result={}", if result.is_ok() { @@ -76,12 +76,12 @@ pub fn validate_btc_address(addr: &str) -> Result { /// Sender-side validation — must be P2WPKH because we only know how to /// derive + sign for native segwit (`bc1q…`). Recipients can be any type. /// -/// See [`validate_btc_address`] for why this delegates. `tinywallet` keeps the +/// See [`validate_btc_address`] for why this delegates. `tinywallet-bus` keeps the /// two rules as separate functions for the same reason this module does: using /// the recipient rule for a sender accepts an address that only fails later, /// at signing time. pub fn validate_btc_sender_address(addr: &str) -> Result { - let result = tinywallet::address::btc::validate_sender(addr).map_err(|e| e.to_string()); + let result = tinywallet_bus::address::btc::validate_sender(addr).map_err(|e| e.to_string()); debug!( "{LOG_PREFIX} validate_address role=sender result={}", if result.is_ok() { @@ -123,10 +123,15 @@ pub async fn broadcast_raw_hex(tx_hex: &str) -> Result { /// Derive the P2WPKH signing key for `derivation_path` from a BIP-39 mnemonic. /// -/// Delegates to the vendored [`tinywallet`] crate, which owns BIP-32 -/// secp256k1 derivation. Custody stays here: the mnemonic is decrypted from -/// the keyring by this crate and handed over as a `&str` that is not retained. -/// Test-only: production derives inside the wallet module. +/// Test-only, and deliberately on the **root** `tinywallet` crate rather than +/// `tinywallet-bus`: `key` is one of the gates that did not move into the +/// contract crate, because deriving is the module's job. The root crate is a +/// dev-dependency here, so this derivation stack is not linked into the shipped +/// binary. Production derives inside the wallet module, via +/// `modules::wallet::derive_account`. +/// +/// Custody stays here: the mnemonic is decrypted from the keyring by this crate +/// and handed over as a `&str` that is not retained. #[cfg(test)] fn derive_btc_private_key( mnemonic: &str, @@ -206,10 +211,10 @@ pub async fn execute_btc_quote(mut quote: PreparedTransaction) -> Result Result Result ( - tinywallet::address::evm::validate("e.to_address).map_err(|e| { + tinywallet_bus::address::evm::validate("e.to_address).map_err(|e| { format!("invalid EVM recipient address '{}': {e}", quote.to_address) })?, quote.amount_raw.clone(), @@ -156,7 +156,7 @@ pub async fn execute_evm_quote(mut quote: PreparedTransaction) -> Result Result` shape the rest of the /// domain speaks. Accepts exactly what the previous `ethers-core` based check /// accepted — prefixed, unprefixed, and any hex case, but not an uppercase /// `0X` prefix. pub fn validate_evm_address(addr: &str) -> Result { - let result = tinywallet::address::evm::validate(addr).map_err(|e| e.to_string()); + let result = tinywallet_bus::address::evm::validate(addr).map_err(|e| e.to_string()); debug!( "{LOG_PREFIX} validate_address result={}", if result.is_ok() { diff --git a/src/openhuman/web3/wallet/chains/solana.rs b/src/openhuman/web3/wallet/chains/solana.rs index e523905e12..7d6879ad92 100644 --- a/src/openhuman/web3/wallet/chains/solana.rs +++ b/src/openhuman/web3/wallet/chains/solana.rs @@ -63,11 +63,11 @@ struct BlockhashValue { /// Validate a Solana address (a base58 ed25519 public key). /// -/// Delegates to the vendored [`tinywallet`] crate, which owns the address +/// Delegates to the vendored [`tinywallet_bus`] crate, which owns the address /// format; this wrapper keeps the `Result<_, String>` shape the rest of the /// domain speaks. pub fn validate_solana_address(addr: &str) -> Result { - let result = tinywallet::address::solana::validate(addr).map_err(|e| e.to_string()); + let result = tinywallet_bus::address::solana::validate(addr).map_err(|e| e.to_string()); debug!( "{LOG_PREFIX} validate_address result={}", if result.is_ok() { @@ -92,12 +92,17 @@ pub async fn native_balance(address: &str) -> Result { /// Derive the Solana signing key for `derivation_path` from a BIP-39 mnemonic. /// -/// Delegates to the vendored [`tinywallet`] crate, which owns SLIP-0010 -/// ed25519 derivation. The hand-rolled HMAC walk and path parser that used to -/// live here moved there wholesale — nothing about "derive an ed25519 key at a -/// hardened path" is OpenHuman-specific. Custody stays here: the mnemonic -/// arrives already decrypted from the keyring and `tinywallet` never sees a -/// stored secret. +/// Test-only, and deliberately on the **root** `tinywallet` crate rather than +/// `tinywallet-bus`: `key` is one of the gates that did not move into the +/// contract crate. The root crate is a dev-dependency here, so this derivation +/// stack is not linked into the shipped binary. Production derives inside the +/// wallet module, via `modules::wallet::derive_account`. +/// +/// The root crate owns SLIP-0010 ed25519 derivation; the hand-rolled HMAC walk +/// and path parser that used to live here moved there wholesale — nothing about +/// "derive an ed25519 key at a hardened path" is OpenHuman-specific. Custody +/// stays here: the mnemonic arrives already decrypted from the keyring and +/// `tinywallet` never sees a stored secret. /// /// One behavioural note: `tinywallet` reports a non-hardened Solana path as /// its own error variant rather than folding it into a generic parse failure, @@ -254,7 +259,7 @@ fn pubkey_to_b58(pubkey: &[u8; 32]) -> String { /// the attestation guard. The local branch cannot exist in a shipped binary. async fn solana_signer( config: &crate::openhuman::config::Config, -) -> Result<(tinywallet::wire::SecretMaterial, [u8; 32]), String> { +) -> Result<(tinywallet_bus::wire::SecretMaterial, [u8; 32]), String> { let secret = secret_material(WalletChain::Solana).await?; let mnemonic = crate::openhuman::security::encryption::rpc::decrypt_secret( config, @@ -262,10 +267,10 @@ async fn solana_signer( ) .await? .value; - let signing_secret = tinywallet::wire::SecretMaterial { + let signing_secret = tinywallet_bus::wire::SecretMaterial { mnemonic, derivation_path: secret.derivation_path.clone(), - chain: tinywallet::Chain::Solana, + chain: tinywallet_bus::Chain::Solana, }; #[cfg(test)] { @@ -288,7 +293,7 @@ async fn solana_signer( /// Sign `message` with the wallet key, inside the module. async fn solana_sign( config: &crate::openhuman::config::Config, - signing_secret: &tinywallet::wire::SecretMaterial, + signing_secret: &tinywallet_bus::wire::SecretMaterial, message: &[u8], ) -> Result<[u8; 64], String> { #[cfg(test)] @@ -304,13 +309,13 @@ async fn solana_sign( config, signing_secret, message, - tinywallet::wire::Scheme::Ed25519, + tinywallet_bus::wire::Scheme::Ed25519, ) .await .map_err(|e| format!("failed to sign the Solana message: {e}"))?; #[cfg(not(test))] { - let tinywallet::wire::Signature::Ed25519 { signature_hex } = signature else { + let tinywallet_bus::wire::Signature::Ed25519 { signature_hex } = signature else { return Err("the wallet module returned a non-ed25519 Solana signature".to_string()); }; let bytes = hex_to_bytes(&signature_hex)?; @@ -699,10 +704,10 @@ pub(crate) async fn tinyplace_signer_seed() -> Result<[u8; 32], String> { ) .await? .value; - let signing_secret = tinywallet::wire::SecretMaterial { + let signing_secret = tinywallet_bus::wire::SecretMaterial { mnemonic, derivation_path: secret.derivation_path.clone(), - chain: tinywallet::Chain::Solana, + chain: tinywallet_bus::Chain::Solana, }; // The one path that brings a private key back into this process. Every @@ -763,8 +768,9 @@ mod tests { #[test] fn unhardened_paths_are_rejected() { - // Path parsing now lives in `tinywallet`, so this exercises the rule - // through the derivation entry point rather than a private helper. + // Path parsing lives in the root `tinywallet` crate, so this exercises + // the rule through the derivation entry point rather than a private + // helper. const MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon \ abandon abandon abandon abandon abandon about"; assert!(derive_solana_keypair(MNEMONIC, "m/44'/501'/0'/0'").is_ok()); diff --git a/src/openhuman/web3/wallet/chains/tron.rs b/src/openhuman/web3/wallet/chains/tron.rs index 1832205f9c..6318c160a1 100644 --- a/src/openhuman/web3/wallet/chains/tron.rs +++ b/src/openhuman/web3/wallet/chains/tron.rs @@ -27,11 +27,11 @@ const TRC20_FEE_LIMIT_SUN: u64 = 15_000_000; /// Validate a Tron mainnet base58check address. /// -/// Delegates to the vendored [`tinywallet`] crate, which owns the address +/// Delegates to the vendored [`tinywallet_bus`] crate, which owns the address /// format; this wrapper keeps the `Result<_, String>` shape the rest of the /// domain speaks. pub fn validate_tron_address(addr: &str) -> Result { - let result = tinywallet::address::tron::validate(addr).map_err(|e| e.to_string()); + let result = tinywallet_bus::address::tron::validate(addr).map_err(|e| e.to_string()); debug!( "{LOG_PREFIX} validate_address result={}", if result.is_ok() { @@ -46,13 +46,13 @@ pub fn validate_tron_address(addr: &str) -> Result { /// Convert a base58check Tron address into the 42-hex-digit form the TronGrid /// API expects, version prefix included. /// -/// Delegates to [`tinywallet`]. Note this now validates the address before +/// Delegates to [`tinywallet_bus`]. Note this now validates the address before /// converting, where the previous local implementation decoded without a /// length check — a malformed address that happened to base58check-decode to /// the wrong length used to produce a short hex string and fail further /// downstream at the API call. pub fn tron_address_to_hex(addr: &str) -> Result { - let result = tinywallet::address::tron::to_hex(addr).map_err(|e| e.to_string()); + let result = tinywallet_bus::address::tron::to_hex(addr).map_err(|e| e.to_string()); debug!( "{LOG_PREFIX} address_to_hex result={}", if result.is_ok() { @@ -92,27 +92,27 @@ struct TriggerSmartContractResponse { /// What the node was asked to build, for verifying what it returned. /// -/// [`tinywallet::wire::TronTransfer`] is that type — it is already on the +/// [`tinywallet_bus::wire::TronTransfer`] is that type — it is already on the /// host/module wire contract, so a second local mirror of it would be one more /// thing to keep in step for no gain. The one thing it deliberately does not /// carry is the fee limit, because only the caller knows what it pinned; that /// rides alongside as [`verify_contract`]'s last argument. -type TronTransferVerification = tinywallet::wire::TronTransfer; +type TronTransferVerification = tinywallet_bus::wire::TronTransfer; /// Check a node-built Tron transaction, then describe it for the signer. /// -/// The verification itself lives in [`tinywallet::tx::tron::verify_contract`], +/// The verification itself lives in [`tinywallet_bus::tx::tron::verify_contract`], /// which parses `raw_data` structurally. The protobuf reader, the contract /// unwrapping and the per-contract field checks used to be hand-rolled here; /// they are the same rules for every host, so they moved into the crate. What /// stays is the part that is OpenHuman's: the fee limit this client pins, and -/// the [`tinywallet::wire::TransactionSpec`] handed to the wallet module. +/// the [`tinywallet_bus::wire::TransactionSpec`] handed to the wallet module. fn tron_transaction_spec( raw_tx: &CreateTransactionResponse, expected_to: String, transfer: &TronTransferVerification, -) -> Result { - let recomputed_txid = tinywallet::tx::tron::recompute_txid(&raw_tx.raw_data_hex) +) -> Result { + let recomputed_txid = tinywallet_bus::tx::tron::recompute_txid(&raw_tx.raw_data_hex) .map_err(|error| format!("invalid Tron raw_data_hex: {error}"))?; // The fee limit is ours, not the crate's: it is what this client pinned in @@ -122,7 +122,7 @@ fn tron_transaction_spec( TronTransferVerification::Trc20 { .. } => Some(TRC20_FEE_LIMIT_SUN), }; - tinywallet::tx::tron::verify_contract( + tinywallet_bus::tx::tron::verify_contract( &raw_tx.raw_data_hex, &expected_to, &raw_tx.tx_id, @@ -131,7 +131,7 @@ fn tron_transaction_spec( ) .map_err(|error| format!("Tron node response rejected: {error}"))?; - Ok(tinywallet::wire::TransactionSpec::Tron { + Ok(tinywallet_bus::wire::TransactionSpec::Tron { raw_data_hex: raw_tx.raw_data_hex.clone(), expected_to, expected_txid: recomputed_txid, @@ -143,11 +143,16 @@ fn tron_transaction_spec( /// Derive the Tron signing key and its base58check address. /// -/// Delegates to the vendored [`tinywallet`] crate, which owns BIP-32 -/// secp256k1 derivation and the Keccak-then-base58check address construction. -/// The hand-rolled BIP-32 walk and path parser that used to live here moved -/// there wholesale. Custody stays here. -/// Test-only: production derives inside the wallet module. +/// Test-only, and deliberately on the **root** `tinywallet` crate rather than +/// `tinywallet-bus`: `key` is one of the gates that did not move into the +/// contract crate. The root crate is a dev-dependency here, so this derivation +/// stack is not linked into the shipped binary. Production derives inside the +/// wallet module, via `modules::wallet::derive_account`. +/// +/// The root crate owns BIP-32 secp256k1 derivation and the +/// Keccak-then-base58check address construction; the hand-rolled BIP-32 walk +/// and path parser that used to live here moved there wholesale. Custody stays +/// here. #[cfg(test)] fn derive_tron_keypair(mnemonic: &str, derivation_path: &str) -> Result<(Vec, String), String> { let derived = tinywallet::key::derive(tinywallet::Chain::Tron, mnemonic, derivation_path) @@ -252,10 +257,10 @@ pub async fn execute_tron_quote(mut quote: PreparedTransaction) -> Result, number: u64, value: u64) { - out.extend(tinywallet::tx::proto::encode_varint(number << 3)); - out.extend(tinywallet::tx::proto::encode_varint(value)); + out.extend(tinywallet_bus::tx::proto::encode_varint(number << 3)); + out.extend(tinywallet_bus::tx::proto::encode_varint(value)); } fn push_bytes_field(out: &mut Vec, number: u64, value: &[u8]) { - out.extend(tinywallet::tx::proto::encode_varint((number << 3) | 2)); - out.extend(tinywallet::tx::proto::encode_varint(value.len() as u64)); + out.extend(tinywallet_bus::tx::proto::encode_varint((number << 3) | 2)); + out.extend(tinywallet_bus::tx::proto::encode_varint(value.len() as u64)); out.extend(value); } @@ -599,7 +604,7 @@ mod tests { let recipient = payload["to_address"].as_str().unwrap(); let amount = payload["amount"].as_u64().unwrap(); let raw = native_raw(recipient, amount); - let txid = tinywallet::tx::tron::recompute_txid(&raw).unwrap(); + let txid = tinywallet_bus::tx::tron::recompute_txid(&raw).unwrap(); create.lock().push(payload); axum::Json(json!({ "txID": txid, @@ -617,7 +622,7 @@ mod tests { let contract = payload["contract_address"].as_str().unwrap(); let parameter = payload["parameter"].as_str().unwrap(); let raw = trc20_raw(contract, parameter); - let txid = tinywallet::tx::tron::recompute_txid(&raw).unwrap(); + let txid = tinywallet_bus::tx::tron::recompute_txid(&raw).unwrap(); trigger.lock().push(payload); axum::Json(json!({ "transaction": { @@ -658,7 +663,7 @@ mod tests { let contract_hex = tron_address_to_hex(contract).unwrap(); let native_raw_hex = native_raw(&recipient_hex, 1_000_000); - let native_txid = tinywallet::tx::tron::recompute_txid(&native_raw_hex).unwrap(); + let native_txid = tinywallet_bus::tx::tron::recompute_txid(&native_raw_hex).unwrap(); let native_tx = CreateTransactionResponse { tx_id: native_txid.clone(), raw_data: json!({}), @@ -674,7 +679,7 @@ mod tests { .unwrap(); assert_eq!( native, - tinywallet::wire::TransactionSpec::Tron { + tinywallet_bus::wire::TransactionSpec::Tron { raw_data_hex: native_raw_hex, expected_to: recipient.to_string(), expected_txid: native_txid, @@ -688,7 +693,7 @@ mod tests { let parameter = "01".repeat(64); let token_raw = trc20_raw(&contract_hex, ¶meter); - let token_txid = tinywallet::tx::tron::recompute_txid(&token_raw).unwrap(); + let token_txid = tinywallet_bus::tx::tron::recompute_txid(&token_raw).unwrap(); let token_tx = CreateTransactionResponse { tx_id: token_txid.clone(), raw_data: json!({}), @@ -704,7 +709,7 @@ mod tests { .unwrap(); assert_eq!( token, - tinywallet::wire::TransactionSpec::Tron { + tinywallet_bus::wire::TransactionSpec::Tron { raw_data_hex: token_raw, expected_to: contract.to_string(), expected_txid: token_txid, @@ -753,7 +758,7 @@ mod tests { ), ] { let altered_tx = CreateTransactionResponse { - tx_id: tinywallet::tx::tron::recompute_txid(&raw_data_hex).unwrap(), + tx_id: tinywallet_bus::tx::tron::recompute_txid(&raw_data_hex).unwrap(), raw_data: json!({}), raw_data_hex, }; @@ -772,11 +777,11 @@ mod tests { // satisfy validation when the selected contract pays something else. let mut spoofed_raw = hex::decode(native_raw(&contract_hex, 2)).unwrap(); let mut decoy = hex::decode(&recipient_hex).unwrap(); - decoy.extend(tinywallet::tx::proto::encode_varint(1_000_000)); + decoy.extend(tinywallet_bus::tx::proto::encode_varint(1_000_000)); push_bytes_field(&mut spoofed_raw, 10, &decoy); let spoofed_raw = hex::encode(spoofed_raw); let spoofed_tx = CreateTransactionResponse { - tx_id: tinywallet::tx::tron::recompute_txid(&spoofed_raw).unwrap(), + tx_id: tinywallet_bus::tx::tron::recompute_txid(&spoofed_raw).unwrap(), raw_data: json!({}), raw_data_hex: spoofed_raw, }; @@ -942,7 +947,7 @@ mod tests { let recipient = payload["to_address"].as_str().unwrap(); let amount = payload["amount"].as_u64().unwrap(); let raw = native_raw(recipient, amount); - let txid = tinywallet::tx::tron::recompute_txid(&raw).unwrap(); + let txid = tinywallet_bus::tx::tron::recompute_txid(&raw).unwrap(); axum::Json(json!({ "txID": txid, "raw_data": {"contract": []}, diff --git a/src/openhuman/web3/wallet/execution.rs b/src/openhuman/web3/wallet/execution.rs index 8527718438..e0c102543b 100644 --- a/src/openhuman/web3/wallet/execution.rs +++ b/src/openhuman/web3/wallet/execution.rs @@ -346,10 +346,10 @@ pub(crate) fn validate_amount(raw: &str) -> Result { /// Validate `addr` for `chain`, returning it trimmed. /// -/// Every arm delegates to the vendored [`tinywallet`] crate, which owns the +/// Every arm delegates to the vendored [`tinywallet_bus`] crate, which owns the /// four address formats. The dispatch stays here rather than calling -/// `tinywallet::address::validate` directly because [`WalletChain`] is -/// OpenHuman's enum, and mapping it onto `tinywallet::Chain` here keeps that +/// `tinywallet_bus::address::validate` directly because [`WalletChain`] is +/// OpenHuman's enum, and mapping it onto `tinywallet_bus::Chain` here keeps that /// translation in one place. /// /// For Bitcoin this is the **recipient** rule — any well-formed mainnet @@ -358,13 +358,13 @@ pub(crate) fn validate_amount(raw: &str) -> Result { /// the other three chains, so it cannot be expressed through this entry point. fn validate_address(chain: WalletChain, addr: &str) -> Result { let tw_chain = match chain { - WalletChain::Evm => tinywallet::Chain::Evm, - WalletChain::Btc => tinywallet::Chain::Btc, - WalletChain::Solana => tinywallet::Chain::Solana, - WalletChain::Tron => tinywallet::Chain::Tron, + WalletChain::Evm => tinywallet_bus::Chain::Evm, + WalletChain::Btc => tinywallet_bus::Chain::Btc, + WalletChain::Solana => tinywallet_bus::Chain::Solana, + WalletChain::Tron => tinywallet_bus::Chain::Tron, }; - debug!("{LOG_PREFIX} validate_address chain={chain:?} role=recipient dispatch=tinywallet"); - let result = tinywallet::address::validate(tw_chain, addr).map_err(|e| e.to_string()); + debug!("{LOG_PREFIX} validate_address chain={chain:?} role=recipient dispatch=tinywallet_bus"); + let result = tinywallet_bus::address::validate(tw_chain, addr).map_err(|e| e.to_string()); debug!( "{LOG_PREFIX} validate_address chain={chain:?} role=recipient result={}", if result.is_ok() { diff --git a/src/openhuman/web3/wallet/transport.rs b/src/openhuman/web3/wallet/transport.rs index 3dba7ede1e..96953d88df 100644 --- a/src/openhuman/web3/wallet/transport.rs +++ b/src/openhuman/web3/wallet/transport.rs @@ -1,8 +1,8 @@ -//! OpenHuman's implementation of the [`tinywallet::rpc::Transport`] seam. +//! OpenHuman's implementation of the [`tinywallet_bus::rpc::Transport`] seam. //! -//! `tinywallet` performs no I/O and takes no URLs: it names a -//! [`NetworkId`](tinywallet::rpc::NetworkId) and asks a host to reach it. This -//! module is that host side — the adapter that lets `tinywallet::rpc` and +//! `tinywallet-bus` performs no I/O and takes no URLs: it names a +//! [`NetworkId`](tinywallet_bus::rpc::NetworkId) and asks a host to reach it. This +//! module is that host side — the adapter that lets `tinywallet_bus::rpc` and //! the chain modules run against OpenHuman's existing RPC layer. //! //! Everything the crate deliberately refused to own lives on this side of the @@ -16,7 +16,7 @@ //! //! ## Error mapping is the part worth reviewing //! -//! `tinywallet` splits transport failures into retryable +//! `tinywallet-bus` splits transport failures into retryable //! ([`TransportError::Unreachable`]) and authoritative //! ([`TransportError::Rpc`]), and a host's failover depends on that //! distinction: retrying an authoritative "insufficient funds" gets the same @@ -32,7 +32,7 @@ use async_trait::async_trait; use log::debug; use serde_json::Value; -use tinywallet::rpc::{NetworkId, Transport, TransportError, TransportResult}; +use tinywallet_bus::rpc::{NetworkId, Transport, TransportError, TransportResult}; use super::defaults::{rpc_url_for_chain, rpc_url_for_evm_network, EvmNetwork}; use super::ops::WalletChain; @@ -53,13 +53,13 @@ impl OpenHumanTransport { } } -/// Map a `tinywallet` network onto OpenHuman's chain enum plus a base URL. +/// Map a `tinywallet-bus` network onto OpenHuman's chain enum plus a base URL. #[allow(unreachable_patterns)] fn resolve(network: NetworkId) -> Result { match network.chain { - tinywallet::Chain::Evm => { + tinywallet_bus::Chain::Evm => { // An EVM request names its EIP-155 chain id; resolving it here is - // what keeps `tinywallet` free of OpenHuman's network enum. + // what keeps `tinywallet-bus` free of OpenHuman's network enum. let chain_id = network.evm_chain_id.ok_or_else(|| TransportError::Rpc { network, message: "EVM requests require an EIP-155 chain id".to_string(), @@ -74,10 +74,10 @@ fn resolve(network: NetworkId) -> Result { })?; Ok(rpc_url_for_evm_network(evm)) } - tinywallet::Chain::Btc => Ok(rpc_url_for_chain(WalletChain::Btc)), - tinywallet::Chain::Solana => Ok(rpc_url_for_chain(WalletChain::Solana)), - tinywallet::Chain::Tron => Ok(rpc_url_for_chain(WalletChain::Tron)), - // `tinywallet::Chain` is `#[non_exhaustive]`, so a future variant must + tinywallet_bus::Chain::Btc => Ok(rpc_url_for_chain(WalletChain::Btc)), + tinywallet_bus::Chain::Solana => Ok(rpc_url_for_chain(WalletChain::Solana)), + tinywallet_bus::Chain::Tron => Ok(rpc_url_for_chain(WalletChain::Tron)), + // `tinywallet_bus::Chain` is `#[non_exhaustive]`, so a future variant must // be handled. Reporting it as authoritative is correct: no endpoint is // configured for it, and retrying elsewhere cannot change that. other => Err(TransportError::Rpc { @@ -238,7 +238,7 @@ mod tests { #[test] fn an_evm_request_without_a_chain_id_is_authoritative_not_retryable() { - let err = resolve(NetworkId::chain(tinywallet::Chain::Evm)).unwrap_err(); + let err = resolve(NetworkId::chain(tinywallet_bus::Chain::Evm)).unwrap_err(); assert!(!err.is_retryable(), "{err}"); } @@ -253,9 +253,9 @@ mod tests { #[test] fn every_non_evm_chain_resolves() { for chain in [ - tinywallet::Chain::Btc, - tinywallet::Chain::Solana, - tinywallet::Chain::Tron, + tinywallet_bus::Chain::Btc, + tinywallet_bus::Chain::Solana, + tinywallet_bus::Chain::Tron, ] { assert!(resolve(NetworkId::chain(chain)).is_ok(), "{chain}"); } @@ -265,7 +265,7 @@ mod tests { fn transport_failures_are_retryable_and_everything_else_is_not() { // The conservative direction: only what this layer knows to be a // transport failure may drive a failover. - let network = NetworkId::chain(tinywallet::Chain::Btc); + let network = NetworkId::chain(tinywallet_bus::Chain::Btc); assert!( classify(network, "wallet RPC transport failed for x: refused".into()).is_retryable() ); diff --git a/src/openhuman/web3/x402/ops.rs b/src/openhuman/web3/x402/ops.rs index a3b1cbfa8f..1f418d7978 100644 --- a/src/openhuman/web3/x402/ops.rs +++ b/src/openhuman/web3/x402/ops.rs @@ -308,7 +308,7 @@ pub async fn handle_402_and_pay( async fn wallet_signer() -> Result< ( crate::openhuman::config::Config, - tinywallet::wire::SecretMaterial, + tinywallet_bus::wire::SecretMaterial, [u8; 32], ), X402Error, @@ -331,10 +331,10 @@ async fn wallet_signer() -> Result< .map_err(|e| X402Error::Wallet(format!("decrypt mnemonic: {e}")))? .value; - let signing_secret = tinywallet::wire::SecretMaterial { + let signing_secret = tinywallet_bus::wire::SecretMaterial { mnemonic, derivation_path: secret.derivation_path.clone(), - chain: tinywallet::Chain::Solana, + chain: tinywallet_bus::Chain::Solana, }; let account = crate::openhuman::modules::wallet::derive_account(&config, &signing_secret) .await @@ -453,7 +453,7 @@ fn parse_settlement_response(b64_str: &str) -> Result Result { - use tinywallet::eip712; + use tinywallet_bus::eip712; let chain_id = req .evm_chain_id() @@ -742,16 +742,32 @@ pub(crate) fn evm_payment_payload( }) } -/// Derive the wallet's EVM signing key from the encrypted mnemonic. +/// Resolve the EVM account an x402 payment will be signed as. /// -/// Returns the raw secret and the checksummed address it controls. Derivation -/// goes through `tinywallet::key` — the same BIP-32 walk the wallet domain uses, -/// so an x402 payment is signed by exactly the account the wallet reports — and -/// the key stays in this process. +/// Returns the config, the [`SecretMaterial`](tinywallet_bus::wire::SecretMaterial) +/// the signing calls take, and the checksummed address that material controls. +/// +/// # Where the key is, and where it is not +/// +/// **No private key is derived in this process.** The address comes back from +/// `modules::wallet::derive_account` — a confidential call into the loaded +/// `tinywallet` module, which derives, answers with public data only, and wipes +/// its copy of the phrase before returning. The signature is produced the same +/// way, by `modules::wallet::sign_message`. This binary links no derivation +/// stack at all: it takes `tinywallet-bus`, the wire contract, and `key` is one +/// of the gates that deliberately stayed in the root crate. +/// +/// What *does* live in this process is the decrypted **mnemonic**, held in the +/// returned `SecretMaterial` for as long as a caller holds it and sent across +/// the bus on each confidential call. That is the exposure to reason about +/// here; a derived private key is not. +/// +/// Deriving the address rather than assuming one is what makes an x402 payment +/// signed by exactly the account the wallet reports. async fn evm_signer() -> Result< ( crate::openhuman::config::Config, - tinywallet::wire::SecretMaterial, + tinywallet_bus::wire::SecretMaterial, String, ), X402Error, @@ -774,10 +790,10 @@ async fn evm_signer() -> Result< .map_err(|e| X402Error::Wallet(format!("decrypt mnemonic: {e}")))? .value; - let signing_secret = tinywallet::wire::SecretMaterial { + let signing_secret = tinywallet_bus::wire::SecretMaterial { mnemonic, derivation_path: secret.derivation_path.clone(), - chain: tinywallet::Chain::Evm, + chain: tinywallet_bus::Chain::Evm, }; let account = crate::openhuman::modules::wallet::derive_account(&config, &signing_secret) .await @@ -823,7 +839,7 @@ pub(crate) fn build_evm_payment_with_signer( /// The 20 raw bytes of an EVM address. fn evm_address_bytes(address: &str) -> Result<[u8; 20], X402Error> { - let validated = tinywallet::address::evm::validate(address) + let validated = tinywallet_bus::address::evm::validate(address) .map_err(|e| X402Error::Protocol(format!("invalid EVM address '{address}': {e}")))?; let body = validated.strip_prefix("0x").unwrap_or(&validated); let decoded = hex::decode(body) diff --git a/src/openhuman/web3/x402/x402_tests.rs b/src/openhuman/web3/x402/x402_tests.rs index a0e3ad8147..7c1c59e564 100644 --- a/src/openhuman/web3/x402/x402_tests.rs +++ b/src/openhuman/web3/x402/x402_tests.rs @@ -394,11 +394,11 @@ fn solana_payment_proof_serializes_correctly() { #[test] fn eip712_domain_separator_is_deterministic() { - // Now `tinywallet::eip712`; the crate's own suite pins its hashes against + // Now `tinywallet_bus::eip712`; the crate's own suite pins its hashes against // the published EIP-712/EIP-3009 vectors. What this test checks is the // property that matters at this layer: the separator is deterministic and // binds the chain, so an authorization cannot be replayed on another one. - use tinywallet::eip712::domain_separator; + use tinywallet_bus::eip712::domain_separator; let contract = base_usdc(); let sep1 = domain_separator(contract, 8453, "USD Coin", "2"); @@ -411,8 +411,11 @@ fn eip712_domain_separator_is_deterministic() { /// The BIP-39 vector mnemonic's EVM account: raw secret and its address. /// -/// Derived through `tinywallet::key`, which is what the production path uses, -/// so the test signs as exactly the account the wallet would. +/// Derived through the **root** `tinywallet` crate's `key` gate, taken here as a +/// dev-dependency. Production does not derive in this process at all — it calls +/// `modules::wallet::derive_account` — so this reproduces the same BIP-32 walk +/// locally, which is what lets the test sign as exactly the account the wallet +/// would without standing up a broker. fn test_signer() -> (Vec, String) { let test_mnemonic = "abandon abandon abandon abandon abandon abandon \ abandon abandon abandon abandon abandon about"; @@ -436,7 +439,7 @@ fn address_bytes(hex: &str) -> [u8; 20] { #[test] fn eip3009_struct_hash_is_deterministic() { - use tinywallet::eip712::{transfer_with_authorization_hash, u256_from_u64}; + use tinywallet_bus::eip712::{transfer_with_authorization_hash, u256_from_u64}; let from = address_bytes(&"aa".repeat(20)); let to = address_bytes(&"bb".repeat(20)); @@ -532,7 +535,7 @@ fn build_evm_payment_with_test_key_produces_valid_payload() { assert_eq!(evm.authorization.value, "2500"); assert_eq!(evm.authorization.valid_after, "0"); assert!(evm.authorization.nonce.starts_with("0x")); - // Checksummed, as `tinywallet::address::evm` renders it, and as + // Checksummed, as `tinywallet_bus::address::evm` renders it, and as // the requirement itself carried it. assert_eq!( evm.authorization.to, @@ -541,7 +544,7 @@ fn build_evm_payment_with_test_key_produces_valid_payload() { assert_eq!(evm.authorization.from, from_address); use k256::ecdsa::{RecoveryId, Signature, VerifyingKey}; - use tinywallet::eip712; + use tinywallet_bus::eip712; let raw = hex::decode(evm.signature.trim_start_matches("0x")).unwrap(); assert!(matches!(raw[64], 27 | 28), "invalid recovery byte"); diff --git a/vendor/tinywallet b/vendor/tinywallet index 22141ecaab..fdce4afca3 160000 --- a/vendor/tinywallet +++ b/vendor/tinywallet @@ -1 +1 @@ -Subproject commit 22141ecaab2b6022dc9970bedc9fc29b65ee7b45 +Subproject commit fdce4afca32f90367f246636f0cb7249a8164598