From 039d328d6d727ee07d6eb473a37aaf792729c8d4 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 12 Oct 2026 15:28:02 +0400 Subject: [PATCH 1/3] fix: bind native Solana approvals to transfer facts Co-authored-by: Medulla --- .../src/crypto/chains/solana/mod.rs | 38 +++++++++--- .../src/crypto/chains/solana/mod_tests.rs | 59 +++++++++++++++---- 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/crates/tinywallet-web3/src/crypto/chains/solana/mod.rs b/crates/tinywallet-web3/src/crypto/chains/solana/mod.rs index 9936a12b..9546d3b0 100644 --- a/crates/tinywallet-web3/src/crypto/chains/solana/mod.rs +++ b/crates/tinywallet-web3/src/crypto/chains/solana/mod.rs @@ -20,7 +20,7 @@ use base64::engine::general_purpose::STANDARD as B64; use log::debug; use serde::Deserialize; use serde_json::json; -use tinywallet_bus::wire::{Scheme, Signature}; +use tinywallet_bus::wire::{Scheme, Signature, TransactionSpec}; use crate::crypto::execution::{ ExecutionResult, PreparedKind, PreparedStatus, PreparedTransaction, @@ -218,14 +218,34 @@ pub(crate) async fn execute_solana_quote( )); } - let to_pubkey = b58_to_pubkey(&to_addr)?; - let message_bytes = transfer_message(engine, "e, from_pk, to_pubkey, amount).await?; - - let sig_bytes = solana_sign(engine, &message_bytes).await?; - let mut wire = Vec::with_capacity(1 + 64 + message_bytes.len()); - wire.extend(encode_shortvec(1)); - wire.extend(sig_bytes); - wire.extend(&message_bytes); + let wire = match quote.kind { + PreparedKind::NativeTransfer => { + let recent_blockhash = fetch_recent_blockhash(engine).await?; + let transaction = TransactionSpec::Solana { + from: from_addr, + to: to_addr, + lamports: amount, + recent_blockhash: pubkey_to_b58(&recent_blockhash), + }; + let signed = engine + .signer + .sign_transaction(WalletChain::Solana, &transaction) + .await?; + B64.decode(signed.raw) + .map_err(|error| format!("invalid signed Solana transaction: {error}"))? + } + PreparedKind::TokenTransfer => { + let to_pubkey = b58_to_pubkey(&to_addr)?; + let message_bytes = + transfer_message(engine, "e, from_pk, to_pubkey, amount).await?; + let sig_bytes = solana_sign(engine, &message_bytes).await?; + let mut wire = Vec::with_capacity(1 + 64 + message_bytes.len()); + wire.extend(encode_shortvec(1)); + wire.extend(sig_bytes); + wire.extend(&message_bytes); + wire + } + }; let tx_sig = broadcast_solana(engine, &wire).await?; quote.status = PreparedStatus::Broadcasted; diff --git a/crates/tinywallet-web3/src/crypto/chains/solana/mod_tests.rs b/crates/tinywallet-web3/src/crypto/chains/solana/mod_tests.rs index d621b336..a9931eb0 100644 --- a/crates/tinywallet-web3/src/crypto/chains/solana/mod_tests.rs +++ b/crates/tinywallet-web3/src/crypto/chains/solana/mod_tests.rs @@ -7,7 +7,7 @@ use base64::Engine as _; use base64::engine::general_purpose::STANDARD as B64; use ed25519_dalek::Verifier as _; use serde_json::{Value, json}; -use tinywallet_bus::wire::Signature; +use tinywallet_bus::wire::{Signature, TransactionSpec}; use super::wire::{b58_to_pubkey, encode_shortvec, token_program_id}; use super::{ @@ -93,6 +93,8 @@ async fn balance_reads_lamports() { async fn a_native_transfer_is_signed_by_the_wallet_and_broadcast() { let rig = Rig::new(); script_node(&rig); + rig.signer + .set_raw(&B64.encode(b"signed native transaction")); let result = execute_solana_quote(&rig.engine, sol_quote(PreparedKind::NativeTransfer)) .await .unwrap(); @@ -112,15 +114,7 @@ async fn a_native_transfer_is_signed_by_the_wallet_and_broadcast() { .collect(); assert_eq!(methods, ["getLatestBlockhash", "sendTransaction"]); - // The broadcast wire is one signature slot plus the message, and the - // signature verifies against the wallet's key. - let wire = broadcast_wire(&rig); - assert_eq!(wire[0], 1, "exactly one signature"); - let (sig, message) = wire[1..].split_at(64); - let key = FakeSigner::solana_key().verifying_key(); - let signature = ed25519_dalek::Signature::from_slice(sig).unwrap(); - key.verify(message, &signature) - .expect("the broadcast signature is valid over the message"); + assert_eq!(broadcast_wire(&rig), b"signed native transaction"); assert!( rig.signer .calls() @@ -128,6 +122,33 @@ async fn a_native_transfer_is_signed_by_the_wallet_and_broadcast() { ); } +#[tokio::test] +async fn a_native_transfer_uses_quote_bound_transaction_signing() { + let rig = Rig::new(); + script_node(&rig); + let quote = sol_quote(PreparedKind::NativeTransfer); + execute_solana_quote(&rig.engine, quote.clone()) + .await + .unwrap(); + + assert_eq!( + rig.signer.transactions(), + vec![TransactionSpec::Solana { + from: quote.from_address, + to: quote.to_address, + lamports: 1000, + recent_blockhash: BLOCKHASH.to_string(), + }] + ); + assert!( + !rig.signer + .calls() + .iter() + .any(|call| matches!(call, SignerCall::Message(..))), + "native SOL transfers must not use opaque message signing" + ); +} + #[tokio::test] async fn a_devnet_transfer_links_to_the_devnet_explorer() { let rig = Rig::new(); @@ -262,9 +283,13 @@ async fn signer_failures_are_surfaced_verbatim() { let rig = Rig::new(); script_node(&rig); + rig.transport.on_rpc( + "getAccountInfo", + json!({"context": {"slot": 0}, "value": {"lamports": 2_039_280u64, "owner": "Tokenkeg", "data": ["", "base64"]}}), + ); rig.signer .fail_message("failed to sign the Solana message: module unavailable"); - let err = execute_solana_quote(&rig.engine, sol_quote(PreparedKind::NativeTransfer)) + let err = execute_solana_quote(&rig.engine, sol_quote(PreparedKind::TokenTransfer)) .await .unwrap_err(); assert_eq!(err, "failed to sign the Solana message: module unavailable"); @@ -302,8 +327,12 @@ async fn a_wrong_kind_or_malformed_signature_from_the_signer_is_refused() { for (reply, expected) in cases { let rig = Rig::new(); script_node(&rig); + rig.transport.on_rpc( + "getAccountInfo", + json!({"context": {"slot": 0}, "value": {"lamports": 2_039_280u64, "owner": "Tokenkeg", "data": ["", "base64"]}}), + ); rig.signer.reply_message(reply); - let err = execute_solana_quote(&rig.engine, sol_quote(PreparedKind::NativeTransfer)) + let err = execute_solana_quote(&rig.engine, sol_quote(PreparedKind::TokenTransfer)) .await .unwrap_err(); assert!(err.starts_with(expected), "{err}"); @@ -315,10 +344,14 @@ async fn a_wrong_kind_or_malformed_signature_from_the_signer_is_refused() { async fn a_non_ascii_signature_is_an_error_not_a_panic() { let rig = Rig::new(); script_node(&rig); + rig.transport.on_rpc( + "getAccountInfo", + json!({"context": {"slot": 0}, "value": {"lamports": 2_039_280u64, "owner": "Tokenkeg", "data": ["", "base64"]}}), + ); rig.signer.reply_message(Signature::Ed25519 { signature_hex: "é".repeat(64), }); - let err = execute_solana_quote(&rig.engine, sol_quote(PreparedKind::NativeTransfer)) + let err = execute_solana_quote(&rig.engine, sol_quote(PreparedKind::TokenTransfer)) .await .unwrap_err(); assert!(err.contains("from the wallet module"), "{err}"); From 7dad7b2fb3027e41326bbc3f9820356dc996a00c Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 12 Oct 2026 15:35:42 +0400 Subject: [PATCH 2/3] test: verify signed Solana transfer facts Co-authored-by: Medulla --- .../src/service/web3_native_wallet_tests.rs | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs b/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs index b53e10e8..df1c9e2c 100644 --- a/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs +++ b/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs @@ -19,6 +19,20 @@ fn fixture_varint(mut value: u64) -> Vec { result } +fn read_shortvec(bytes: &[u8], offset: &mut usize) -> usize { + let mut value = 0usize; + let mut shift = 0; + loop { + let byte = bytes[*offset]; + *offset += 1; + value |= usize::from(byte & 0x7f) << shift; + if byte & 0x80 == 0 { + return value; + } + shift += 7; + } +} + #[tokio::test] #[allow( clippy::too_many_lines, @@ -402,6 +416,154 @@ async fn native_wallet_signer_authorizes_exact_transaction_facts_for_all_chains( broker_task.abort(); } +#[tokio::test] +async fn native_solana_signing_decodes_to_the_approved_transfer() { + use base64::Engine as _; + use tinywallet_bus::wire::{ + NativeWalletExecutionIntent, PreparedKind, PreparedStatus, PreparedTransaction, + SecretMaterial, TransactionSpec, + }; + use tinywallet_web3::crypto::seams::WalletSigner; + use tinywallet_web3::crypto::wallet::WalletChain; + + const BLOCKHASH: &str = "GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi"; + const RECIPIENT: &str = "Vote111111111111111111111111111111111111111"; + const LAMPORTS: u64 = 9_876; + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let derivation_path = "m/44'/501'/0'/0'"; + let sender = tinywallet::key::derive(tinywallet_bus::Chain::Solana, mnemonic, derivation_path) + .unwrap() + .address() + .to_string(); + let transaction = TransactionSpec::Solana { + from: sender.clone(), + to: RECIPIENT.to_string(), + lamports: LAMPORTS, + recent_blockhash: BLOCKHASH.to_string(), + }; + let quote = PreparedTransaction { + quote_id: "solana-native-signing-test".to_string(), + kind: PreparedKind::NativeTransfer, + chain: WalletChain::Solana, + evm_network: None, + from_address: sender.clone(), + to_address: RECIPIENT.to_string(), + asset_symbol: "SOL".to_string(), + amount_raw: LAMPORTS.to_string(), + amount_formatted: "0.000009876 SOL".to_string(), + receive_symbol: None, + min_receive_raw: None, + calldata: None, + token_address: None, + estimated_fee_raw: "5000".to_string(), + status: PreparedStatus::AwaitingConfirmation, + created_at_ms: 1, + expires_at_ms: u64::MAX, + notes: Vec::new(), + }; + let (callbacks, _, native_requests, _, _host, broker_task) = callbacks().await; + let signer = ModuleSigner(callbacks.clone()); + let approval = NativeApprovalContext { + callbacks, + quote: quote.clone(), + }; + let secret = SecretMaterial { + mnemonic: mnemonic.to_string(), + derivation_path: derivation_path.to_string(), + chain: tinywallet_bus::Chain::Solana, + }; + let signed = with_execution_context(None, secret, None, async { + with_native_approval( + approval, + signer.sign_transaction(WalletChain::Solana, &transaction), + ) + .await + }) + .await + .unwrap(); + + let requests = native_requests.lock().unwrap(); + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].quote, quote); + assert_eq!( + requests[0].intent, + NativeWalletExecutionIntent::Transaction { transaction } + ); + drop(requests); + + let wire = base64::engine::general_purpose::STANDARD + .decode(signed.raw) + .unwrap(); + let mut offset = 0; + assert_eq!(read_shortvec(&wire, &mut offset), 1, "one signature slot"); + offset += 64; + let message = &wire[offset..]; + assert_eq!(&message[..3], &[1, 0, 1], "legacy transfer header"); + let mut message_offset = 3; + assert_eq!( + read_shortvec(message, &mut message_offset), + 3, + "payer, recipient, system program" + ); + let keys_start = message_offset; + let keys_end = keys_start + 3 * 32; + let keys = &message[keys_start..keys_end]; + assert_eq!( + &keys[..32], + &tinywallet::address::solana::decode(&sender).unwrap(), + "payer matches the approved sender" + ); + assert_eq!( + &keys[32..64], + &tinywallet::address::solana::decode(RECIPIENT).unwrap(), + "recipient matches the approved transfer" + ); + assert_eq!( + &message[keys_end..keys_end + 32], + &tinywallet::address::solana::decode(BLOCKHASH).unwrap(), + "blockhash matches the typed transaction" + ); + message_offset = keys_end + 32; + assert_eq!( + read_shortvec(message, &mut message_offset), + 1, + "one system transfer instruction" + ); + assert_eq!(message[message_offset], 2, "system program account index"); + message_offset += 1; + assert_eq!( + read_shortvec(message, &mut message_offset), + 2, + "sender and recipient accounts" + ); + assert_eq!(&message[message_offset..message_offset + 2], &[0, 1]); + message_offset += 2; + assert_eq!( + read_shortvec(message, &mut message_offset), + 12, + "system transfer instruction length" + ); + assert_eq!( + u32::from_le_bytes( + message[message_offset..message_offset + 4] + .try_into() + .unwrap() + ), + 2, + "SystemInstruction::Transfer" + ); + assert_eq!( + u64::from_le_bytes( + message[message_offset + 4..message_offset + 12] + .try_into() + .unwrap() + ), + LAMPORTS, + "lamports match the approved quote" + ); + broker_task.abort(); +} + #[tokio::test] async fn native_signer_can_derive_and_then_sign_with_one_scoped_secret() { use tinywallet_bus::wire::{Scheme, SecretMaterial, TransactionSpec, TronTransfer}; From 69e41322515761827a5a53b54b03b0cdb58b23d5 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 12 Oct 2026 15:49:07 +0400 Subject: [PATCH 3/3] test: verify signed Solana transfer facts Co-authored-by: Medulla --- .../src/service/web3_native_wallet_tests.rs | 165 +----------------- .../native_solana_signing_tests.rs | 154 ++++++++++++++++ 2 files changed, 157 insertions(+), 162 deletions(-) create mode 100644 crates/tinywallet-module/src/service/web3_native_wallet_tests/native_solana_signing_tests.rs diff --git a/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs b/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs index df1c9e2c..492c7946 100644 --- a/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs +++ b/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs @@ -19,20 +19,6 @@ fn fixture_varint(mut value: u64) -> Vec { result } -fn read_shortvec(bytes: &[u8], offset: &mut usize) -> usize { - let mut value = 0usize; - let mut shift = 0; - loop { - let byte = bytes[*offset]; - *offset += 1; - value |= usize::from(byte & 0x7f) << shift; - if byte & 0x80 == 0 { - return value; - } - shift += 7; - } -} - #[tokio::test] #[allow( clippy::too_many_lines, @@ -416,154 +402,6 @@ async fn native_wallet_signer_authorizes_exact_transaction_facts_for_all_chains( broker_task.abort(); } -#[tokio::test] -async fn native_solana_signing_decodes_to_the_approved_transfer() { - use base64::Engine as _; - use tinywallet_bus::wire::{ - NativeWalletExecutionIntent, PreparedKind, PreparedStatus, PreparedTransaction, - SecretMaterial, TransactionSpec, - }; - use tinywallet_web3::crypto::seams::WalletSigner; - use tinywallet_web3::crypto::wallet::WalletChain; - - const BLOCKHASH: &str = "GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi"; - const RECIPIENT: &str = "Vote111111111111111111111111111111111111111"; - const LAMPORTS: u64 = 9_876; - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - let derivation_path = "m/44'/501'/0'/0'"; - let sender = tinywallet::key::derive(tinywallet_bus::Chain::Solana, mnemonic, derivation_path) - .unwrap() - .address() - .to_string(); - let transaction = TransactionSpec::Solana { - from: sender.clone(), - to: RECIPIENT.to_string(), - lamports: LAMPORTS, - recent_blockhash: BLOCKHASH.to_string(), - }; - let quote = PreparedTransaction { - quote_id: "solana-native-signing-test".to_string(), - kind: PreparedKind::NativeTransfer, - chain: WalletChain::Solana, - evm_network: None, - from_address: sender.clone(), - to_address: RECIPIENT.to_string(), - asset_symbol: "SOL".to_string(), - amount_raw: LAMPORTS.to_string(), - amount_formatted: "0.000009876 SOL".to_string(), - receive_symbol: None, - min_receive_raw: None, - calldata: None, - token_address: None, - estimated_fee_raw: "5000".to_string(), - status: PreparedStatus::AwaitingConfirmation, - created_at_ms: 1, - expires_at_ms: u64::MAX, - notes: Vec::new(), - }; - let (callbacks, _, native_requests, _, _host, broker_task) = callbacks().await; - let signer = ModuleSigner(callbacks.clone()); - let approval = NativeApprovalContext { - callbacks, - quote: quote.clone(), - }; - let secret = SecretMaterial { - mnemonic: mnemonic.to_string(), - derivation_path: derivation_path.to_string(), - chain: tinywallet_bus::Chain::Solana, - }; - let signed = with_execution_context(None, secret, None, async { - with_native_approval( - approval, - signer.sign_transaction(WalletChain::Solana, &transaction), - ) - .await - }) - .await - .unwrap(); - - let requests = native_requests.lock().unwrap(); - assert_eq!(requests.len(), 1); - assert_eq!(requests[0].quote, quote); - assert_eq!( - requests[0].intent, - NativeWalletExecutionIntent::Transaction { transaction } - ); - drop(requests); - - let wire = base64::engine::general_purpose::STANDARD - .decode(signed.raw) - .unwrap(); - let mut offset = 0; - assert_eq!(read_shortvec(&wire, &mut offset), 1, "one signature slot"); - offset += 64; - let message = &wire[offset..]; - assert_eq!(&message[..3], &[1, 0, 1], "legacy transfer header"); - let mut message_offset = 3; - assert_eq!( - read_shortvec(message, &mut message_offset), - 3, - "payer, recipient, system program" - ); - let keys_start = message_offset; - let keys_end = keys_start + 3 * 32; - let keys = &message[keys_start..keys_end]; - assert_eq!( - &keys[..32], - &tinywallet::address::solana::decode(&sender).unwrap(), - "payer matches the approved sender" - ); - assert_eq!( - &keys[32..64], - &tinywallet::address::solana::decode(RECIPIENT).unwrap(), - "recipient matches the approved transfer" - ); - assert_eq!( - &message[keys_end..keys_end + 32], - &tinywallet::address::solana::decode(BLOCKHASH).unwrap(), - "blockhash matches the typed transaction" - ); - message_offset = keys_end + 32; - assert_eq!( - read_shortvec(message, &mut message_offset), - 1, - "one system transfer instruction" - ); - assert_eq!(message[message_offset], 2, "system program account index"); - message_offset += 1; - assert_eq!( - read_shortvec(message, &mut message_offset), - 2, - "sender and recipient accounts" - ); - assert_eq!(&message[message_offset..message_offset + 2], &[0, 1]); - message_offset += 2; - assert_eq!( - read_shortvec(message, &mut message_offset), - 12, - "system transfer instruction length" - ); - assert_eq!( - u32::from_le_bytes( - message[message_offset..message_offset + 4] - .try_into() - .unwrap() - ), - 2, - "SystemInstruction::Transfer" - ); - assert_eq!( - u64::from_le_bytes( - message[message_offset + 4..message_offset + 12] - .try_into() - .unwrap() - ), - LAMPORTS, - "lamports match the approved quote" - ); - broker_task.abort(); -} - #[tokio::test] async fn native_signer_can_derive_and_then_sign_with_one_scoped_secret() { use tinywallet_bus::wire::{Scheme, SecretMaterial, TransactionSpec, TronTransfer}; @@ -648,6 +486,9 @@ async fn native_signer_can_derive_and_then_sign_with_one_scoped_secret() { broker_task.abort(); } +#[path = "web3_native_wallet_tests/native_solana_signing_tests.rs"] +mod native_solana_signing_tests; + struct LegacyHost; #[tinybus::interface(name = "ai.tinyhumans.tinywallet.Host")] diff --git a/crates/tinywallet-module/src/service/web3_native_wallet_tests/native_solana_signing_tests.rs b/crates/tinywallet-module/src/service/web3_native_wallet_tests/native_solana_signing_tests.rs new file mode 100644 index 00000000..00b7d27d --- /dev/null +++ b/crates/tinywallet-module/src/service/web3_native_wallet_tests/native_solana_signing_tests.rs @@ -0,0 +1,154 @@ +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use super::*; + +use base64::Engine as _; +use tinywallet_bus::wire::{ + NativeWalletExecutionIntent, PreparedKind, PreparedStatus, PreparedTransaction, SecretMaterial, + TransactionSpec, +}; +use tinywallet_web3::crypto::seams::WalletSigner; +use tinywallet_web3::crypto::wallet::WalletChain; + +const BLOCKHASH: &str = "GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi"; +const RECIPIENT: &str = "Vote111111111111111111111111111111111111111"; +const LAMPORTS: u64 = 9_876; + +fn read_shortvec(bytes: &[u8], offset: &mut usize) -> usize { + let mut value = 0usize; + let mut shift = 0; + loop { + let byte = bytes[*offset]; + *offset += 1; + value |= usize::from(byte & 0x7f) << shift; + if byte & 0x80 == 0 { + return value; + } + shift += 7; + } +} + +#[tokio::test] +async fn native_solana_signing_decodes_to_the_approved_transfer() { + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let derivation_path = "m/44'/501'/0'/0'"; + let sender = tinywallet::key::derive(tinywallet_bus::Chain::Solana, mnemonic, derivation_path) + .unwrap() + .address() + .to_string(); + let transaction = TransactionSpec::Solana { + from: sender.clone(), + to: RECIPIENT.to_string(), + lamports: LAMPORTS, + recent_blockhash: BLOCKHASH.to_string(), + }; + let quote = PreparedTransaction { + quote_id: "solana-native-signing-test".to_string(), + kind: PreparedKind::NativeTransfer, + chain: WalletChain::Solana, + evm_network: None, + from_address: sender.clone(), + to_address: RECIPIENT.to_string(), + asset_symbol: "SOL".to_string(), + amount_raw: LAMPORTS.to_string(), + amount_formatted: "0.000009876 SOL".to_string(), + receive_symbol: None, + min_receive_raw: None, + calldata: None, + token_address: None, + estimated_fee_raw: "5000".to_string(), + status: PreparedStatus::AwaitingConfirmation, + created_at_ms: 1, + expires_at_ms: u64::MAX, + notes: Vec::new(), + }; + let (callbacks, _, native_requests, _, _host, broker_task) = callbacks().await; + let signer = ModuleSigner(callbacks.clone()); + let approval = NativeApprovalContext { + callbacks, + quote: quote.clone(), + }; + let secret = SecretMaterial { + mnemonic: mnemonic.to_string(), + derivation_path: derivation_path.to_string(), + chain: tinywallet_bus::Chain::Solana, + }; + let signed_transaction = with_execution_context(None, secret, None, async { + with_native_approval( + approval, + signer.sign_transaction(WalletChain::Solana, &transaction), + ) + .await + }) + .await + .unwrap(); + + let requests = native_requests.lock().unwrap(); + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].quote, quote); + assert_eq!( + requests[0].intent, + NativeWalletExecutionIntent::Transaction { transaction } + ); + drop(requests); + + assert_signed_wire_matches_quote(&signed_transaction.raw, &sender, RECIPIENT, LAMPORTS); + broker_task.abort(); +} + +fn assert_signed_wire_matches_quote(raw: &str, sender: &str, recipient: &str, lamports: u64) { + let wire = base64::engine::general_purpose::STANDARD + .decode(raw) + .unwrap(); + let mut offset = 0; + assert_eq!(read_shortvec(&wire, &mut offset), 1, "one signature slot"); + offset += 64; + let message = &wire[offset..]; + assert_eq!(&message[..3], &[1, 0, 1], "legacy transfer header"); + let mut message_offset = 3; + assert_eq!(read_shortvec(message, &mut message_offset), 3); + let keys_start = message_offset; + let keys_end = keys_start + 3 * 32; + let keys = &message[keys_start..keys_end]; + assert_eq!( + &keys[..32], + &tinywallet::address::solana::decode(sender).unwrap(), + "payer matches the approved sender" + ); + assert_eq!( + &keys[32..64], + &tinywallet::address::solana::decode(recipient).unwrap(), + "recipient matches the approved transfer" + ); + assert_eq!( + &message[keys_end..keys_end + 32], + &tinywallet::address::solana::decode(BLOCKHASH).unwrap(), + "blockhash matches the typed transaction" + ); + message_offset = keys_end + 32; + assert_eq!(read_shortvec(message, &mut message_offset), 1); + assert_eq!(message[message_offset], 2, "system program account index"); + message_offset += 1; + assert_eq!(read_shortvec(message, &mut message_offset), 2); + assert_eq!(&message[message_offset..message_offset + 2], &[0, 1]); + message_offset += 2; + assert_eq!(read_shortvec(message, &mut message_offset), 12); + assert_eq!( + u32::from_le_bytes( + message[message_offset..message_offset + 4] + .try_into() + .unwrap() + ), + 2, + "SystemInstruction::Transfer" + ); + assert_eq!( + u64::from_le_bytes( + message[message_offset + 4..message_offset + 12] + .try_into() + .unwrap() + ), + lamports, + "lamports match the approved quote" + ); +}