From 96a6d34315602587589b35546d31ebdfa40fe096 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:37:01 +0300 Subject: [PATCH 01/19] chore: rename `tinywallet` to `tinywallet_bus` across all wallet and x402 modules Updated every import, type reference, and doc comment that referred to the `tinywallet` crate to use its new name `tinywallet_bus`, reflecting the crate's repackaging for the bus-based module architecture. No behaviour changes. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/modules/wallet.rs | 2 +- src/openhuman/modules/wallet_tests.rs | 14 +++--- src/openhuman/web3/wallet/abi.rs | 8 +-- src/openhuman/web3/wallet/chains/btc.rs | 16 +++--- src/openhuman/web3/wallet/chains/evm.rs | 16 +++--- src/openhuman/web3/wallet/chains/solana.rs | 22 ++++---- src/openhuman/web3/wallet/chains/tron.rs | 58 +++++++++++----------- src/openhuman/web3/wallet/execution.rs | 16 +++--- src/openhuman/web3/wallet/transport.rs | 28 +++++------ src/openhuman/web3/x402/ops.rs | 28 +++++------ src/openhuman/web3/x402/x402_tests.rs | 12 ++--- 11 files changed, 110 insertions(+), 110 deletions(-) diff --git a/src/openhuman/modules/wallet.rs b/src/openhuman/modules/wallet.rs index 1dcfb6e80d..66f1157ce8 100644 --- a/src/openhuman/modules/wallet.rs +++ b/src/openhuman/modules/wallet.rs @@ -66,7 +66,7 @@ //! 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::wire::{ DerivedAccount, ExportRequest, ExportedKey, Scheme, SecretMaterial, SignMessageRequest, SignRequest, Signature, SignedTransaction, TransactionSpec, }; diff --git a/src/openhuman/modules/wallet_tests.rs b/src/openhuman/modules/wallet_tests.rs index f018d0ffe1..126fb4adac 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::{Scheme, Signature, SigningPayload, 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(), diff --git a/src/openhuman/web3/wallet/abi.rs b/src/openhuman/web3/wallet/abi.rs index 10a8c3577e..4f9cce9fcb 100644 --- a/src/openhuman/web3/wallet/abi.rs +++ b/src/openhuman/web3/wallet/abi.rs @@ -5,7 +5,7 @@ //! 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,14 +27,14 @@ /// 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 { .. } => { + 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::abi::Error::InvalidAmount { .. } => { + tinywallet_bus::abi::Error::InvalidAmount { .. } => { format!("amount '{amount_raw}' is not a valid non-negative integer") } _ => error.to_string(), diff --git a/src/openhuman/web3/wallet/chains/btc.rs b/src/openhuman/web3/wallet/chains/btc.rs index 31662c0a16..d96466b224 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() { @@ -81,7 +81,7 @@ pub fn validate_btc_address(addr: &str) -> Result { /// 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,7 +123,7 @@ 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 +/// Delegates to the vendored [`tinywallet_bus`] 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. @@ -206,10 +206,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..751be6b5f2 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,7 +92,7 @@ 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 +/// Delegates to the vendored [`tinywallet_bus`] 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 @@ -254,7 +254,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 +262,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 +288,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 +304,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 +699,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 diff --git a/src/openhuman/web3/wallet/chains/tron.rs b/src/openhuman/web3/wallet/chains/tron.rs index 1832205f9c..3eaba69004 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,7 +143,7 @@ fn tron_transaction_spec( /// Derive the Tron signing key and its base58check address. /// -/// Delegates to the vendored [`tinywallet`] crate, which owns BIP-32 +/// Delegates to the vendored [`tinywallet_bus`] 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. @@ -252,10 +252,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 +599,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 +617,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 +658,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 +674,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 +688,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 +704,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 +753,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 +772,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 +942,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..a7e48870b0 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()); + 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..85138cbcb9 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 +//! [`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 @@ -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; @@ -57,7 +57,7 @@ impl OpenHumanTransport { #[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. let chain_id = network.evm_chain_id.ok_or_else(|| TransportError::Rpc { @@ -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..85544e1aa5 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() @@ -745,13 +745,13 @@ pub(crate) fn evm_payment_payload( /// Derive the wallet's EVM signing key from the encrypted mnemonic. /// /// Returns the raw secret and the checksummed address it controls. Derivation -/// goes through `tinywallet::key` — the same BIP-32 walk the wallet domain uses, +/// goes through `tinywallet_bus::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. async fn evm_signer() -> Result< ( crate::openhuman::config::Config, - tinywallet::wire::SecretMaterial, + tinywallet_bus::wire::SecretMaterial, String, ), X402Error, @@ -774,10 +774,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 +823,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..446a187801 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,7 +411,7 @@ 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, +/// Derived through `tinywallet_bus::key`, which is what the production path uses, /// so the test signs as exactly the account the wallet would. fn test_signer() -> (Vec, String) { let test_mnemonic = "abandon abandon abandon abandon abandon abandon \ @@ -436,7 +436,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 +532,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 +541,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"); From 4494d0ed80b34f11c25e7f6d6b271c13caac32c0 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:37:18 +0300 Subject: [PATCH 02/19] chore(deps): replace tinywallet with tinywallet-bus contract crate The dependency on the root `tinywallet` crate is replaced with the smaller `tinywallet-bus` contract crate, which contains only the wire types, bus member names, address validation, EIP-712 and ERC-20 encoders, the Transport seam, and the Tron verifier. This avoids pulling in the `bitcoin` crate and its native secp256k1 build, since key derivation, transaction building, and signing now happen inside the loaded tinywallet module. The root crate remains available under dev-dependencies for test fixtures without affecting the shipped binary. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.toml | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index be8a2607d6..b1b8d303d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -524,23 +524,22 @@ tinyhosts = { path = "vendor/tinyhosts", default-features = false, features = [" # 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 } +# +# 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 +847,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", ] From f49bb7500d82343020f32b80c0a0e011a5b252b3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:38:11 +0300 Subject: [PATCH 03/19] chore(deps): update tinywallet subproject commit Updated the pinned commit for the tinywallet vendored dependency to incorporate upstream fixes or improvements. Auto-committed-on: dragonfly Co-authored-by: Medulla --- vendor/tinywallet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/tinywallet b/vendor/tinywallet index 22141ecaab..34c1e65b44 160000 --- a/vendor/tinywallet +++ b/vendor/tinywallet @@ -1 +1 @@ -Subproject commit 22141ecaab2b6022dc9970bedc9fc29b65ee7b45 +Subproject commit 34c1e65b44a46525410fb6a7ab4d2e5bd543025b From 36b2a9f143a4484363034b3704aa2236ea2dcbd1 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:38:17 +0300 Subject: [PATCH 04/19] chore(deps): update Cargo.lock for tinywallet-bus extraction The lock file is updated to reflect the extraction of the tinywallet-bus crate from tinywallet, adding the new dependency entry and adjusting the dependency lists accordingly. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.lock | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f409adc6e1..155c96f17d 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", @@ -6724,21 +6725,32 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "tinywallet" version = "0.4.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.4.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]] From e2a08b0aff795a3eb4689df77ee329e65be8499d Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:38:28 +0300 Subject: [PATCH 05/19] refactor(wallet): replace inline method strings with constants from tinywallet-bus Replace hardcoded method name strings with the corresponding constants from the `tinywallet_bus::names::methods` module to centralise method name definitions and reduce the risk of typos or mismatches when the bus protocol evolves. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/modules/wallet.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/openhuman/modules/wallet.rs b/src/openhuman/modules/wallet.rs index 66f1157ce8..d45f08988e 100644 --- a/src/openhuman/modules/wallet.rs +++ b/src/openhuman/modules/wallet.rs @@ -66,6 +66,7 @@ //! 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_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(), },), From 95358d7e22e27bf36acee826decc9913dcf25ccb Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:38:53 +0300 Subject: [PATCH 06/19] test(wallet): add contract tests for registry entry and method membership Add two tests that verify the compiled-in registry entry for tinywallet matches the interface this client calls, and that every method this client invokes is declared in the bus contract. These tests catch mismatches that would otherwise surface only as runtime errors in the field. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/modules/wallet_tests.rs | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/openhuman/modules/wallet_tests.rs b/src/openhuman/modules/wallet_tests.rs index 126fb4adac..a613ff50bf 100644 --- a/src/openhuman/modules/wallet_tests.rs +++ b/src/openhuman/modules/wallet_tests.rs @@ -245,3 +245,62 @@ 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" + ); + } + } +} From 1f1ba791a61934fd27f831e917674dc7ba13688e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:39:48 +0300 Subject: [PATCH 07/19] docs(AGENTS): document the tinywallet-bus extraction and its design rationale Add a detailed explanation of the tinywallet-bus crate's role, clarifying that it holds the wire contract, bus member names, address formats, EIP-712 and ERC-20 encoders, and the Tron verification codec, while the root tinywallet crate survives only as a dev-dependency. Also update the crate gating table to reflect that both tinydocs-bus and tinywallet-bus are taken with default-features = false, and add a note that member names must come from constants rather than literals to prevent runtime drift. Auto-committed-on: dragonfly Co-authored-by: Medulla --- AGENTS.md | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 79d0431a4c..ed648f170d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -207,6 +207,16 @@ 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`. +**Both are taken as their `-bus` contract crate, not as the repository's root +crate.** `vendor/tinydocs/crates/tinydocs-bus` and +`vendor/tinywallet/crates/tinywallet-bus` are transport-free libraries holding +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 crates in those repositories hold the implementation the TinyBus module +carries (`.docx` synthesis; key derivation, transaction building and signing), +and this binary does not link them. Same shape as `tinyvoice-bus`, +`tinyjuice-bus` and `tinyruntime-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 +225,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 +242,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 +251,30 @@ 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-bus` + set with its spec validators. +- **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**: `tinydocs-bus` 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` From 28a5fcce3f34ea0f6a06d9e1415e895a6fbb6dd5 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:40:25 +0300 Subject: [PATCH 08/19] docs(AGENTS.md): clarify bus crate description and update tinydocs reference Reworded the explanation of how the binary consumes bus contract crates to be clearer about the relationship between bus crates and root crates, and updated the tinydocs reference in the consequences section to match the established pattern of describing bus crate ownership of host-side rules. Auto-committed-on: dragonfly Co-authored-by: Medulla --- AGENTS.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ed648f170d..7201ff35ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -207,15 +207,13 @@ 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`. -**Both are taken as their `-bus` contract crate, not as the repository's root -crate.** `vendor/tinydocs/crates/tinydocs-bus` and -`vendor/tinywallet/crates/tinywallet-bus` are transport-free libraries holding -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 crates in those repositories hold the implementation the TinyBus module -carries (`.docx` synthesis; key derivation, transaction building and signing), -and this binary does not link them. Same shape as `tinyvoice-bus`, -`tinyjuice-bus` and `tinyruntime-bus`. +**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 @@ -257,8 +255,9 @@ Consequences worth knowing before touching either seam: 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-bus` - set with its spec validators. + 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` / @@ -270,7 +269,7 @@ Consequences worth knowing before touching either seam: 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**: `tinydocs-bus` is +- **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 From 49e9deb7eefc9f669ff23f919a40777ba5c3585e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:43:57 +0300 Subject: [PATCH 09/19] chore(registry): update tinywallet module to version 0.5.0 Updated the tinywallet module record in the registry from version 0.4.0 to 0.5.0, including the release URL and all platform-specific asset archives with their corresponding SHA-256 checksums. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/modules/registry.rs | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/openhuman/modules/registry.rs b/src/openhuman/modules/registry.rs index 28ac510513..280ee9a53e 100644 --- a/src/openhuman/modules/registry.rs +++ b/src/openhuman/modules/registry.rs @@ -131,63 +131,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, From 1f945e890b10a77e569dd4d469088304bbf75832 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:44:21 +0300 Subject: [PATCH 10/19] chore(tinywallet): update subproject commit Update the vendor/tinywallet subproject to point at a newer commit, incorporating upstream changes. Auto-committed-on: dragonfly Co-authored-by: Medulla --- vendor/tinywallet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/tinywallet b/vendor/tinywallet index 34c1e65b44..fdce4afca3 160000 --- a/vendor/tinywallet +++ b/vendor/tinywallet @@ -1 +1 @@ -Subproject commit 34c1e65b44a46525410fb6a7ab4d2e5bd543025b +Subproject commit fdce4afca32f90367f246636f0cb7249a8164598 From 98606066c23c2599421166c8353c9428bb99c856 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:44:27 +0300 Subject: [PATCH 11/19] chore(deps): bump tinywallet and tinywallet-bus to 0.5.0 Update the version numbers for both the tinywallet and tinywallet-bus crates in the lock file from 0.4.0 to 0.5.0, reflecting the new release. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 155c96f17d..9b1b94b0a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6723,7 +6723,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tinywallet" -version = "0.4.0" +version = "0.5.0" dependencies = [ "coins-bip32 0.8.7", "coins-bip39 0.13.1", @@ -6739,7 +6739,7 @@ dependencies = [ [[package]] name = "tinywallet-bus" -version = "0.4.0" +version = "0.5.0" dependencies = [ "async-trait", "bech32 0.11.1", From 97b779918953704966da168b4b87e63dac4476c3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:49:18 +0300 Subject: [PATCH 12/19] chore(abi): reformat error mapping in encode_erc20_transfer Reformatted the error mapping closure in `encode_erc20_transfer` to use a single expression with a trailing comma, and removed an unnecessary line break in the test file. This is a purely cosmetic change with no behavioural impact. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/modules/wallet_tests.rs | 3 +-- src/openhuman/web3/wallet/abi.rs | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/openhuman/modules/wallet_tests.rs b/src/openhuman/modules/wallet_tests.rs index a613ff50bf..6ca95a710d 100644 --- a/src/openhuman/modules/wallet_tests.rs +++ b/src/openhuman/modules/wallet_tests.rs @@ -261,8 +261,7 @@ mod contract { #[test] fn the_registry_entry_matches_the_interface_this_client_calls() { - let record = - registry::find("tinywallet").expect("the tinywallet record is compiled in"); + 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!( diff --git a/src/openhuman/web3/wallet/abi.rs b/src/openhuman/web3/wallet/abi.rs index 4f9cce9fcb..91cfb2c11d 100644 --- a/src/openhuman/web3/wallet/abi.rs +++ b/src/openhuman/web3/wallet/abi.rs @@ -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_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(), - }) + 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)] From 705c3021ea0d5a4f3a24fa64b41392e731f01777 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:53:58 +0300 Subject: [PATCH 13/19] chore(deps): update tinywallet dependency to tinywallet-bus v0.5.0 Updated the Cargo.lock file to replace the tinywallet dependency with tinywallet-bus version 0.5.0, reflecting a rename and version bump in the dependency tree. Auto-committed-on: dragonfly Co-authored-by: Medulla --- app/src-tauri/Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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", From 46b519bd7f5709695d1e47b63b40dc7f1e8a621c Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 12:56:37 +0300 Subject: [PATCH 14/19] chore: files changed src/openhuman/modules/wallet_tests.rs Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/modules/wallet_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openhuman/modules/wallet_tests.rs b/src/openhuman/modules/wallet_tests.rs index 6ca95a710d..e1632ba1fc 100644 --- a/src/openhuman/modules/wallet_tests.rs +++ b/src/openhuman/modules/wallet_tests.rs @@ -7,7 +7,7 @@ //! themselves are covered where they can be honest: `tinywallet`'s own loader //! E2E, which drives a real module over a real broker. -use tinywallet_bus::wire::{Scheme, Signature, SigningPayload, TransactionSpec}; +use tinywallet_bus::wire::TransactionSpec; use tinywallet_bus::Chain; use super::{classify, WalletCallError}; From 605276f1fb3f4496914b3cb11ba702b5f8bbee41 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 14:10:43 +0300 Subject: [PATCH 15/19] chore: update doc references from `tinywallet` to `tinywallet-bus` and clarify key derivation bounda Updated documentation across the wallet and x402 modules to reflect the split between the root `tinywallet` crate and the `tinywallet-bus` contract crate, correcting stale references and explaining which derivation paths live where. The key change is that private key derivation now happens only in the root crate or via confidential module calls, not in the bus crate, and the doc comments across all chain modules and the x402 signer now accurately describe this architecture. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/web3/wallet/abi.rs | 2 +- src/openhuman/web3/wallet/chains/btc.rs | 17 +++++++++----- src/openhuman/web3/wallet/chains/solana.rs | 17 +++++++++----- src/openhuman/web3/wallet/chains/tron.rs | 15 ++++++++----- src/openhuman/web3/wallet/execution.rs | 2 +- src/openhuman/web3/wallet/transport.rs | 8 +++---- src/openhuman/web3/x402/ops.rs | 26 +++++++++++++++++----- src/openhuman/web3/x402/x402_tests.rs | 7 ++++-- 8 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/openhuman/web3/wallet/abi.rs b/src/openhuman/web3/wallet/abi.rs index 91cfb2c11d..8750482eb3 100644 --- a/src/openhuman/web3/wallet/abi.rs +++ b/src/openhuman/web3/wallet/abi.rs @@ -1,4 +1,4 @@ -//! 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 diff --git a/src/openhuman/web3/wallet/chains/btc.rs b/src/openhuman/web3/wallet/chains/btc.rs index d96466b224..40f831e10a 100644 --- a/src/openhuman/web3/wallet/chains/btc.rs +++ b/src/openhuman/web3/wallet/chains/btc.rs @@ -76,7 +76,7 @@ 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. @@ -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_bus`] 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, @@ -422,7 +427,7 @@ mod tests { #[test] fn validate_btc_address_rejects_testnet() { let err = validate_btc_address("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx").unwrap_err(); - // `tinywallet` reports a wrong-network address as a distinct condition + // `tinywallet-bus` reports a wrong-network address as a distinct condition // from a malformed one, so the message names the required network. assert!(err.contains("not on mainnet"), "got: {err}"); } diff --git a/src/openhuman/web3/wallet/chains/solana.rs b/src/openhuman/web3/wallet/chains/solana.rs index 751be6b5f2..c0aa569983 100644 --- a/src/openhuman/web3/wallet/chains/solana.rs +++ b/src/openhuman/web3/wallet/chains/solana.rs @@ -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_bus`] 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, diff --git a/src/openhuman/web3/wallet/chains/tron.rs b/src/openhuman/web3/wallet/chains/tron.rs index 3eaba69004..6318c160a1 100644 --- a/src/openhuman/web3/wallet/chains/tron.rs +++ b/src/openhuman/web3/wallet/chains/tron.rs @@ -143,11 +143,16 @@ fn tron_transaction_spec( /// Derive the Tron signing key and its base58check address. /// -/// Delegates to the vendored [`tinywallet_bus`] 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) diff --git a/src/openhuman/web3/wallet/execution.rs b/src/openhuman/web3/wallet/execution.rs index a7e48870b0..e0c102543b 100644 --- a/src/openhuman/web3/wallet/execution.rs +++ b/src/openhuman/web3/wallet/execution.rs @@ -363,7 +363,7 @@ fn validate_address(chain: WalletChain, addr: &str) -> Result { WalletChain::Solana => tinywallet_bus::Chain::Solana, WalletChain::Tron => tinywallet_bus::Chain::Tron, }; - debug!("{LOG_PREFIX} validate_address chain={chain:?} role=recipient dispatch=tinywallet"); + 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={}", diff --git a/src/openhuman/web3/wallet/transport.rs b/src/openhuman/web3/wallet/transport.rs index 85138cbcb9..96953d88df 100644 --- a/src/openhuman/web3/wallet/transport.rs +++ b/src/openhuman/web3/wallet/transport.rs @@ -1,6 +1,6 @@ //! OpenHuman's implementation of the [`tinywallet_bus::rpc::Transport`] seam. //! -//! `tinywallet` performs no I/O and takes no URLs: it names a +//! `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. @@ -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 @@ -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_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(), diff --git a/src/openhuman/web3/x402/ops.rs b/src/openhuman/web3/x402/ops.rs index 85544e1aa5..2180e496af 100644 --- a/src/openhuman/web3/x402/ops.rs +++ b/src/openhuman/web3/x402/ops.rs @@ -742,12 +742,28 @@ 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_bus::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 one of them. +/// +/// 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, diff --git a/src/openhuman/web3/x402/x402_tests.rs b/src/openhuman/web3/x402/x402_tests.rs index 446a187801..7c1c59e564 100644 --- a/src/openhuman/web3/x402/x402_tests.rs +++ b/src/openhuman/web3/x402/x402_tests.rs @@ -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_bus::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"; From ce4ad9246181de24ff38523b054ac8aef958d760 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 14:11:19 +0300 Subject: [PATCH 16/19] chore(docs): clarify feature-gating and dependency notes Updated the Cargo.toml comment for the tinywallet dependency to explain that the web3 feature is off in the contributor default set and on in the product set, so a bare cargo check does not pay for it. Refined the registry.rs doc comment to state that the binary does not link the root tinywallet crate at all, only the tinywallet-bus contract crate, and updated the btc.rs test comment to clarify that the derived address comes from the root tinywallet crate running inside the wallet module in production. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.toml | 5 ++++- src/openhuman/modules/registry.rs | 6 ++++-- src/openhuman/web3/wallet/chains/btc.rs | 6 ++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b1b8d303d0..06703e0bbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -523,7 +523,10 @@ 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. +# 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 diff --git a/src/openhuman/modules/registry.rs b/src/openhuman/modules/registry.rs index 280ee9a53e..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 diff --git a/src/openhuman/web3/wallet/chains/btc.rs b/src/openhuman/web3/wallet/chains/btc.rs index 40f831e10a..3230dcf675 100644 --- a/src/openhuman/web3/wallet/chains/btc.rs +++ b/src/openhuman/web3/wallet/chains/btc.rs @@ -664,8 +664,10 @@ mod tests { assert!(matches!(pubkey[0], 0x02 | 0x03)); // The known-good vector for this mnemonic and path, unchanged by the - // move off the `bitcoin` crate. Derived through `tinywallet`, which is - // the same code the address in `execute_btc_quote` comes from. + // move off the `bitcoin` crate. Derived through the root `tinywallet` + // crate, which is the same code that produces the address in + // `execute_btc_quote` — in production it runs inside the wallet module + // rather than here, but it is the same crate and the same walk. let derived = tinywallet::key::derive(tinywallet::Chain::Btc, mnemonic, "m/84'/0'/0'/0/0").unwrap(); assert_eq!( From c940dd0ff0add5355cde23ca2499216011903bb3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 14:12:22 +0300 Subject: [PATCH 17/19] fix(tests): update comment to reflect tinywallet crate location The comment in the unhardened paths test was updated to clarify that path parsing lives in the root `tinywallet` crate rather than a submodule, making the documentation more accurate for developers reading the test. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/web3/wallet/chains/solana.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openhuman/web3/wallet/chains/solana.rs b/src/openhuman/web3/wallet/chains/solana.rs index c0aa569983..13aa582db2 100644 --- a/src/openhuman/web3/wallet/chains/solana.rs +++ b/src/openhuman/web3/wallet/chains/solana.rs @@ -768,7 +768,7 @@ mod tests { #[test] fn unhardened_paths_are_rejected() { - // Path parsing now lives in `tinywallet`, so this exercises the rule + // Path parsing lives in the root `tinywallet` crate, so this exercises the // 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"; From a4fc0ca5bafa98b13978eab6244dc878a49693d7 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 14:13:14 +0300 Subject: [PATCH 18/19] fix(tests): reword comment in unhardened_paths_are_rejected Reworded the comment in the Solana wallet test to improve clarity by specifying that the test exercises "the rule" rather than just "the" through the derivation entry point. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/web3/wallet/chains/solana.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/openhuman/web3/wallet/chains/solana.rs b/src/openhuman/web3/wallet/chains/solana.rs index 13aa582db2..7d6879ad92 100644 --- a/src/openhuman/web3/wallet/chains/solana.rs +++ b/src/openhuman/web3/wallet/chains/solana.rs @@ -768,8 +768,9 @@ mod tests { #[test] fn unhardened_paths_are_rejected() { - // Path parsing lives in the root `tinywallet` crate, so this exercises the - // 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()); From eebac33ae060b1fff769307fcb57fa109c0cc2f3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 23 Aug 2026 14:13:53 +0300 Subject: [PATCH 19/19] fix(ops): correct incomplete sentence in evm_payment_payload doc The doc comment for `evm_payment_payload` had a sentence that ended abruptly with "not one of them" when the intended meaning was simply "is not." This change removes the trailing "one of them" to make the sentence grammatically complete and clearer. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/web3/x402/ops.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openhuman/web3/x402/ops.rs b/src/openhuman/web3/x402/ops.rs index 2180e496af..1f418d7978 100644 --- a/src/openhuman/web3/x402/ops.rs +++ b/src/openhuman/web3/x402/ops.rs @@ -760,7 +760,7 @@ pub(crate) fn evm_payment_payload( /// 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 one of them. +/// 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.