Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions crates/tinywallet-bus/src/lib_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ fn network_request_keeps_legacy_json_and_omits_unset_endpoint_url() {

#[test]
fn the_contract_accepts_its_own_version_and_newer_minors() {
assert_eq!(CONTRACT_VERSION, (1, 12));
assert_eq!(CONTRACT_VERSION, (1, 13));
assert!(is_compatible(CONTRACT_VERSION));
assert!(is_compatible((CONTRACT_VERSION.0, CONTRACT_VERSION.1 + 1)));
assert!(!is_compatible((CONTRACT_VERSION.0 + 1, 0)));
Expand All @@ -136,13 +136,14 @@ fn the_method_list_is_sorted_complete_and_free_of_duplicates() {
METHODS,
"sorted dispatch order, no repeats"
);
assert_eq!(METHODS.len(), 37);
assert_eq!(METHODS.len(), 38);
for member in [
names::methods::ATTACH_SIGNATURE,
names::methods::BUILD_UNSIGNED,
names::methods::CONSTRUCT_EVM_TRANSACTION,
names::methods::DERIVE_ACCOUNT,
names::methods::DERIVE_MNEMONIC_SEED,
names::methods::ENCODE_ERC20_TRANSFER,
names::methods::EXPORT_KEY,
names::methods::GENERATE_MNEMONIC,
names::methods::SIGN_MESSAGE,
Expand Down Expand Up @@ -279,7 +280,7 @@ fn native_wallet_members_are_additive_and_use_one_typed_argument() {
methods::NATIVE_WALLET_TX_RECEIPT,
methods::NATIVE_WALLET_LOOKUP_TX,
];
assert_eq!(METHODS.len(), 37);
assert_eq!(METHODS.len(), 38);
assert!(native_members.iter().all(|member| METHODS.contains(member)));
assert_eq!(HOST_METHODS.len(), 6);
assert!(HOST_METHODS.contains(&host_methods::AUTHORIZE_NATIVE_WALLET_EXECUTION));
Expand Down
3 changes: 3 additions & 0 deletions crates/tinywallet-bus/src/names/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub mod methods {
pub const NATIVE_WALLET_LOOKUP_TX: &str = "NativeWalletLookupTx";
/// Parses a challenge, reserves budget and builds a confidential payment.
pub const X402_PREPARE_PAYMENT: &str = "X402PreparePayment";
/// Encodes ERC-20 transfer calldata using the module-owned ABI implementation.
pub const ENCODE_ERC20_TRANSFER: &str = "EncodeErc20Transfer";
/// Records a retry result and commits or releases its module reservation.
pub const X402_RECORD_PAYMENT: &str = "X402RecordPayment";
/// Reports ledger totals and spending limits.
Expand All @@ -127,6 +129,7 @@ pub const METHODS: &[&str] = &[
methods::CONSTRUCT_EVM_TRANSACTION,
methods::DERIVE_ACCOUNT,
methods::DERIVE_MNEMONIC_SEED,
methods::ENCODE_ERC20_TRANSFER,
methods::EXPORT_KEY,
methods::GENERATE_MNEMONIC,
methods::NATIVE_WALLET_BALANCES,
Expand Down
2 changes: 1 addition & 1 deletion crates/tinywallet-bus/src/version/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! The `TinyWallet` `TinyBus` contract version and its binding rule.

/// The wire contract version this crate defines.
pub const CONTRACT_VERSION: (u32, u32) = (1, 12);
pub const CONTRACT_VERSION: (u32, u32) = (1, 13);

/// Returns whether a host using [`CONTRACT_VERSION`] can bind to `module`.
///
Expand Down
18 changes: 9 additions & 9 deletions crates/tinywallet-bus/src/wire/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ pub use host::{
X402UpdateBudgetRequest,
};
pub use wallet::{
BalanceInfo, ChainStatus, DeriveMnemonicSeedRequest, DeriveMnemonicSeedResponse, EvmNetwork,
ExecutePreparedParams, ExecutionResult, GenerateMnemonicRequest, GenerateMnemonicResponse,
HostEndpointReference, NativeWalletCloseRequest, NativeWalletConfigRequest,
NativeWalletConfigResponse, NativeWalletExecuteRequest, NativeWalletExecutionIntent,
NativeWalletPrepareTransferRequest, NativeWalletServiceHandle, NativeWalletServiceRequest,
NativeWalletTransactionRequest, PrepareTransferParams, PreparedKind, PreparedStatus,
PreparedTransaction, ProviderStatus, RpcEndpointKey, RpcSource, SolanaCluster, SupportedAsset,
TxLookupInfo, TxReceiptInfo, TxState, TxStatusInfo, WalletAssetDefinition, WalletChain,
WalletNetworkDefaults,
BalanceInfo, ChainStatus, DeriveMnemonicSeedRequest, DeriveMnemonicSeedResponse,
EncodeErc20TransferRequest, EncodeErc20TransferResponse, EvmNetwork, ExecutePreparedParams,
ExecutionResult, GenerateMnemonicRequest, GenerateMnemonicResponse, HostEndpointReference,
NativeWalletCloseRequest, NativeWalletConfigRequest, NativeWalletConfigResponse,
NativeWalletExecuteRequest, NativeWalletExecutionIntent, NativeWalletPrepareTransferRequest,
NativeWalletServiceHandle, NativeWalletServiceRequest, NativeWalletTransactionRequest,
PrepareTransferParams, PreparedKind, PreparedStatus, PreparedTransaction, ProviderStatus,
RpcEndpointKey, RpcSource, SolanaCluster, SupportedAsset, TxLookupInfo, TxReceiptInfo, TxState,
TxStatusInfo, WalletAssetDefinition, WalletChain, WalletNetworkDefaults,
};

use crate::Chain;
Expand Down
39 changes: 38 additions & 1 deletion crates/tinywallet-bus/src/wire/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,48 @@
use serde_json::json;

use super::{
AttachRequest, GenerateMnemonicRequest, GenerateMnemonicResponse, PublicKey, Scheme, Signature,
AttachRequest, EncodeErc20TransferRequest, EncodeErc20TransferResponse,
GenerateMnemonicRequest, GenerateMnemonicResponse, PublicKey, Scheme, Signature,
SignedTransaction, SigningPayload, SigningRequest, TransactionSpec, UnsignedTransaction, Utxo,
WalletChain, Web3QuoteKind, Web3QuoteResponse,
};

#[test]
fn erc20_encoding_wire_shape_is_explicit_and_round_trips() {
let request = EncodeErc20TransferRequest {
to_address: "0x1111111111111111111111111111111111111111".into(),
amount_raw: "1000000".into(),
};
let encoded = serde_json::to_value(&request).unwrap();
assert_eq!(
encoded,
json!({
"to_address": "0x1111111111111111111111111111111111111111",
"amount_raw": "1000000"
})
);
assert_eq!(
serde_json::from_value::<EncodeErc20TransferRequest>(encoded).unwrap(),
request
);

let response = EncodeErc20TransferResponse {
calldata: format!(
"0xa9059cbb{}{}{}{}",
"0".repeat(24),
"11".repeat(20),
"0".repeat(63),
"1"
),
};
let encoded = serde_json::to_value(&response).unwrap();
assert_eq!(encoded["calldata"], response.calldata);
assert_eq!(
serde_json::from_value::<EncodeErc20TransferResponse>(encoded).unwrap(),
response
);
}

#[test]
fn mnemonic_generation_wire_shape_is_explicit_and_secret_marked_by_member() {
let request = GenerateMnemonicRequest { word_count: 24 };
Expand Down
18 changes: 18 additions & 0 deletions crates/tinywallet-bus/src/wire/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ use crate::Chain;

use super::{QuoteOwner, SecretMaterial, TransactionSpec};

/// Requests standard ERC-20 `transfer(address,uint256)` calldata.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EncodeErc20TransferRequest {
/// The 20-byte recipient address, with or without an `0x` prefix.
pub to_address: String,
/// The unsigned base-unit amount as a decimal string.
pub amount_raw: String,
}

/// The module-encoded ERC-20 transfer calldata.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EncodeErc20TransferResponse {
/// Hex calldata including the `0x` prefix.
pub calldata: String,
}

/// Requests a freshly generated BIP-39 recovery phrase.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
Expand Down
12 changes: 12 additions & 0 deletions crates/tinywallet-module/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ mod construction;
reason = "the generated TinyBus async trait requires async methods for synchronous cryptographic operations"
)]
impl Wallet {
/// Encode standard ERC-20 transfer calldata inside the module.
async fn encode_erc20_transfer(
&self,
request: tinywallet_bus::wire::EncodeErc20TransferRequest,
) -> BusResult<tinywallet_bus::wire::EncodeErc20TransferResponse> {
let calldata =
tinywallet_x402::abi::encode_erc20_transfer(&request.to_address, &request.amount_raw)
.map_err(|error| invalid_input(error.to_string()))?;
Ok(tinywallet_bus::wire::EncodeErc20TransferResponse { calldata })
}

/// List swap and bridge routes through the host's authenticated backend
/// transport. Route parsing and all follow-on quote behavior stay here.
async fn web3_routes(&self, _request: ()) -> BusResult<serde_json::Value> {
Expand Down Expand Up @@ -1499,6 +1510,7 @@ mod exports {
"AttachSignature",
"DeriveAccount",
"DeriveMnemonicSeed",
"EncodeErc20Transfer",
"GenerateMnemonic",
"SignTransaction",
"ExportKey",
Expand Down
1 change: 1 addition & 0 deletions crates/tinywallet-module/tests/lifecycle_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const EXPECTED_METHODS: &[&str] = &[
"AttachSignature",
"DeriveAccount",
"DeriveMnemonicSeed",
"EncodeErc20Transfer",
"GenerateMnemonic",
"SignTransaction",
"ExportKey",
Expand Down
37 changes: 37 additions & 0 deletions crates/tinywallet-module/tests/module_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use tinywallet::wire::{
UnsignedTransaction,
};
use tinywallet::{Chain, tx};
use tinywallet_bus::wire::{EncodeErc20TransferRequest, EncodeErc20TransferResponse};
use tinywallet_module::{BUS_NAME, OBJECT_PATH};

/// Every method the manifest must declare, in order.
Expand All @@ -39,6 +40,7 @@ const EXPECTED_METHODS: &[&str] = &[
"AttachSignature",
"DeriveAccount",
"DeriveMnemonicSeed",
"EncodeErc20Transfer",
"GenerateMnemonic",
"SignTransaction",
"ExportKey",
Expand Down Expand Up @@ -97,6 +99,7 @@ async fn the_built_module_signs_every_chain_over_a_real_broker() {

validates_addresses_inside_the_artifact(&proxy).await;
constructs_evm_transactions_inside_the_artifact(&proxy).await;
encodes_erc20_transfers_inside_the_artifact(&proxy).await;
signs_an_evm_transfer_identically_to_the_library(&proxy).await;
signs_a_multi_input_bitcoin_spend(&proxy).await;
signs_a_solana_transfer(&proxy).await;
Expand Down Expand Up @@ -268,6 +271,40 @@ async fn signs_an_evm_transfer_identically_to_the_library(proxy: &tinybus::Proxy
);
}

/// The calldata algorithm is executed by the loaded module, not host-linked crypto.
async fn encodes_erc20_transfers_inside_the_artifact(proxy: &tinybus::Proxy) {
let request = EncodeErc20TransferRequest {
to_address: "0x1111111111111111111111111111111111111111".into(),
amount_raw: "1".into(),
};
let response: EncodeErc20TransferResponse = proxy
.call("EncodeErc20Transfer", (request,))
.await
.expect("the module encodes valid ERC-20 transfer calldata");
assert_eq!(
response.calldata,
format!(
"0xa9059cbb{}{}{}{}",
"0".repeat(24),
"11".repeat(20),
"0".repeat(63),
"1"
)
);

let error = proxy
.call::<EncodeErc20TransferResponse>(
"EncodeErc20Transfer",
(EncodeErc20TransferRequest {
to_address: "not-an-address".into(),
amount_raw: "1".into(),
},),
)
.await
.expect_err("invalid recipient is rejected by the module");
assert!(error.to_string().contains("InvalidInput"));
}

/// Several inputs, so the multi-signature ordering crosses the bus for real.
async fn signs_a_multi_input_bitcoin_spend(proxy: &tinybus::Proxy) {
let derived = tinywallet::key::derive(Chain::Btc, VECTOR, "m/84'/0'/0'/0/0").unwrap();
Expand Down
Loading