Swap: Add swap ui flow (BTC/LTC -> ETH-like)
What changed, and why it matters
This commit adds a new on-device user interface flow for cryptocurrency swaps, where a user can exchange Bitcoin or Litecoin for an Ethereum-like coin through a payment request. The code adds validation rules and user confirmation screens so the hardware wallet can show what is being swapped and where the received coins will go. The change is a feature addition with defensive checks, not a clear fix for an existing vulnerability.
Review the new swap flow as part of normal secure-feature auditing. Pay attention to the amount parser, keypath handling for destination account derivation, and whether the UI text could be misleading. No immediate patch action is indicated by the commit alone.
Security signals we found
New UI confirmation flow for cross-chain swaps
Input validation added for swap amount string parsing
Restriction of swap source accounts to BTC/LTC single-sig configurations
Enforcement that CoinPurchaseMemo must be the sole memo
Feature is conditionally compiled based on app-ethereum support
Evidence from the diff
The patch implements CoinPurchaseMemo handling in the BitBox02 Bitcoin signing flow. It adds UI prompts for swap amount and destination account derivation, parsing of the destination amount string, and validation that swap source accounts are BTC/LTC single-sig only. It also enforces that a CoinPurchaseMemo must be the only memo in a payment request. The feature is gated behind the app-ethereum build feature.
Changed components
src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rssrc/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rsInspect captured patch +610 / −1
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
index da22c25..76a5443 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
@@ -62,6 +62,56 @@ fn find_identity(name: &str) -> Option<&Identity> {
IDENTITIES.iter().find(|identity| identity.name == name)
}
+pub(super) fn contains_coin_purchase_memo(payment_request: &pb::BtcPaymentRequestRequest) -> bool {
+ payment_request.memos.iter().any(|memo| {
+ matches!(
+ memo,
+ Memo {
+ memo: Some(memo::Memo::CoinPurchaseMemo(_)),
+ }
+ )
+ })
+}
+
+/// Parses a human-readable coin purchase amount of the form
+/// "<positive-number> <unit>", where the number may be an integer or decimal,
+/// and returns the amount/unit parts.
+fn parse_coin_purchase_amount(amount: &str) -> Result<(&str, &str), Error> {
+ let mut parts = amount.split_ascii_whitespace();
+ let destination_amount = parts.next().ok_or(Error::InvalidInput)?;
+ let destination_unit = parts.next().ok_or(Error::InvalidInput)?;
+ if parts.next().is_some() {
+ return Err(Error::InvalidInput);
+ }
+
+ let mut decimal_parts = destination_amount.split('.');
+ let integer = match decimal_parts.next() {
+ Some(integer) if !integer.is_empty() => integer,
+ _ => return Err(Error::InvalidInput),
+ };
+ if !integer.bytes().all(|b| b.is_ascii_digit()) {
+ return Err(Error::InvalidInput);
+ }
+
+ let fractional = match decimal_parts.next() {
+ Some(fractional) => {
+ if fractional.is_empty() || !fractional.bytes().all(|b| b.is_ascii_digit()) {
+ return Err(Error::InvalidInput);
+ }
+ fractional
+ }
+ None => "",
+ };
+ if decimal_parts.next().is_some() {
+ return Err(Error::InvalidInput);
+ }
+ if integer.bytes().chain(fractional.bytes()).all(|b| b == b'0') {
+ return Err(Error::InvalidInput);
+ }
+
+ Ok((destination_amount, destination_unit))
+}
+
/// Prompt user to verify the payment request.
pub async fn user_verify(
hal: &mut impl crate::hal::Hal,
@@ -100,7 +150,46 @@ pub async fn user_verify(
verify_message::verify(hal, "Memo", "Memo", text_memo.note.as_bytes(), false)
.await?;
}
- // TODO: add CoinPurchaseMemo arm when SwapKit UI is finalized
+ Memo {
+ memo: Some(memo::Memo::CoinPurchaseMemo(coin_purchase_memo)),
+ } => {
+ let swap_body = format!(
+ "{}\nto\n{}",
+ format_amount(coin_params, format_unit, payment_request.total_amount)?,
+ coin_purchase_memo.amount
+ );
+ hal.ui()
+ .confirm(&ConfirmParams {
+ title: "SWAP",
+ body: &swap_body,
+ accept_is_nextarrow: true,
+ ..Default::default()
+ })
+ .await?;
+ let (_, destination_unit) = parse_coin_purchase_amount(&coin_purchase_memo.amount)?;
+ let address_derivation = coin_purchase_memo
+ .address_derivation
+ .as_ref()
+ .ok_or(Error::InvalidInput)?;
+ let destination_account = match address_derivation {
+ memo::coin_purchase_memo::AddressDerivation::Eth(eth) => {
+ eth.keypath
+ .get(2)
+ .ok_or(Error::InvalidInput)?
+ .checked_sub(util::bip32::HARDENED)
+ .ok_or(Error::InvalidInput)?
+ + 1
+ }
+ };
+ hal.ui()
+ .confirm(&ConfirmParams {
+ title: "Receive to",
+ body: &format!("{destination_unit} account #{destination_account}"),
+ accept_is_nextarrow: true,
+ ..Default::default()
+ })
+ .await?;
+ }
_ => return Err(Error::InvalidInput),
}
}
@@ -224,6 +313,9 @@ pub fn validate(
if payment_request.memos.len() > MAX_MEMOS_NUM {
return Err(ValidationError::Other);
}
+ if contains_coin_purchase_memo(payment_request) && payment_request.memos.len() != 1 {
+ return Err(ValidationError::Other);
+ }
for memo in payment_request.memos.iter() {
if let Memo {
memo: Some(memo::Memo::CoinPurchaseMemo(coin_purchase_memo)),
@@ -255,6 +347,8 @@ pub fn validate(
mod tests {
use super::*;
use crate::hal::testing::TestingHal;
+ use crate::hal::testing::ui::Screen;
+ use util::bb02_async::block_on;
fn make_text_memo(note: &str) -> Memo {
Memo {
@@ -314,6 +408,36 @@ mod tests {
assert!(find_identity("Provider").is_none());
}
+ #[test]
+ fn test_parse_coin_purchase_amount() {
+ assert_eq!(parse_coin_purchase_amount("0.25 ETH"), Ok(("0.25", "ETH")));
+ assert_eq!(parse_coin_purchase_amount("1 ETH"), Ok(("1", "ETH")));
+ assert_eq!(
+ parse_coin_purchase_amount("14128 eth"),
+ Ok(("14128", "eth"))
+ );
+ assert_eq!(
+ parse_coin_purchase_amount("3481471947 SC"),
+ Ok(("3481471947", "SC"))
+ );
+
+ for amount in [
+ "",
+ "ETH",
+ "0 ETH",
+ "0.0 ETH",
+ "-1 ETH",
+ ".25 ETH",
+ "1. ETH",
+ "1.2.3 ETH",
+ "foo ETH",
+ "foo bar baz",
+ "1 ETH extra",
+ ] {
+ assert_eq!(parse_coin_purchase_amount(amount), Err(Error::InvalidInput));
+ }
+ }
+
#[test]
fn test_sighash() {
let coin_params = params::get(pb::BtcCoin::Tbtc);
@@ -523,6 +647,54 @@ mod tests {
));
}
+ #[cfg(feature = "app-ethereum")]
+ {
+ // CoinPurchaseMemo must be the only memo in a payment request.
+ for mut payment_request in [
+ pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![
+ make_text_memo("memo"),
+ make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0x773A77b9D32589be03f9132AF759e294f7851be9",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ ),
+ ],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ },
+ pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![
+ make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0x773A77b9D32589be03f9132AF759e294f7851be9",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ ),
+ make_coin_purchase_memo(
+ 60,
+ "0.50 ETH",
+ "0x773A77b9D32589be03f9132AF759e294f7851be9",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ ),
+ ],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ },
+ ] {
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
+ Err(ValidationError::Other)
+ ));
+ }
+ }
+
// Unknown recipient
let payment_request = pb::BtcPaymentRequestRequest {
recipient_name: "Unknown Merchant".into(),
@@ -581,4 +753,182 @@ mod tests {
Err(ValidationError::InvalidSignature)
));
}
+
+ #[test]
+ fn test_user_verify_text_memos() {
+ // Baseline Pocket flow: recipient screen, memo intro, memo contents.
+ let mut mock_hal = TestingHal::new();
+ block_on(user_verify(
+ &mut mock_hal,
+ params::get(pb::BtcCoin::Btc),
+ &pb::BtcPaymentRequestRequest {
+ recipient_name: "POCKET".into(),
+ memos: vec![make_text_memo("Pocket memo")],
+ nonce: vec![],
+ total_amount: 1234567890,
+ signature: vec![],
+ },
+ FormatUnit::Default,
+ ))
+ .unwrap();
+
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![
+ Screen::Recipient {
+ recipient: "POCKET".into(),
+ amount: "12.34567890 BTC".into(),
+ },
+ Screen::Confirm {
+ title: "".into(),
+ body: "Memo from\n\nPOCKET".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Memo".into(),
+ body: "Pocket memo".into(),
+ longtouch: false,
+ },
+ ]
+ );
+ }
+
+ #[cfg(feature = "app-ethereum")]
+ #[test]
+ fn test_user_verify_swap() {
+ // Happy-path swap flow: recipient screen plus two swap-specific confirms.
+ let mut mock_hal = TestingHal::new();
+ block_on(user_verify(
+ &mut mock_hal,
+ params::get(pb::BtcCoin::Btc),
+ &pb::BtcPaymentRequestRequest {
+ recipient_name: "SWAPKIT (Provider)".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0x123",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ )],
+ nonce: vec![],
+ total_amount: 25000000,
+ signature: vec![],
+ },
+ FormatUnit::Default,
+ ))
+ .unwrap();
+
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![
+ Screen::Recipient {
+ recipient: "SWAPKIT (Provider)".into(),
+ amount: "0.25000000 BTC".into(),
+ },
+ Screen::Confirm {
+ title: "SWAP".into(),
+ body: "0.25000000 BTC\nto\n0.25 ETH".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Receive to".into(),
+ body: "ETH account #1".into(),
+ longtouch: false,
+ },
+ ]
+ );
+ }
+
+ #[cfg(feature = "app-ethereum")]
+ #[test]
+ fn test_user_verify_swap_invalid() {
+ // Invalid swap requests that user_verify must reject because the
+ // UI cannot render them safely.
+ let coin_params = params::get(pb::BtcCoin::Btc);
+
+ for payment_request in [
+ // Missing destination derivation, so "Send to" cannot be built.
+ pb::BtcPaymentRequestRequest {
+ recipient_name: "SWAPKIT (Provider)".into(),
+ memos: vec![make_coin_purchase_memo(60, "0.25 ETH", "0x123", None)],
+ nonce: vec![],
+ total_amount: 25000000,
+ signature: vec![],
+ },
+ // Destination keypath is too short to contain an account element.
+ pb::BtcPaymentRequestRequest {
+ recipient_name: "SWAPKIT (Provider)".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0x123",
+ Some(memo::coin_purchase_memo::AddressDerivation::Eth(
+ memo::coin_purchase_memo::EthAddressDerivation {
+ keypath: vec![44 + util::bip32::HARDENED, 60 + util::bip32::HARDENED],
+ },
+ )),
+ )],
+ nonce: vec![],
+ total_amount: 25000000,
+ signature: vec![],
+ },
+ // Destination account element must be hardened.
+ pb::BtcPaymentRequestRequest {
+ recipient_name: "SWAPKIT (Provider)".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0x123",
+ Some(memo::coin_purchase_memo::AddressDerivation::Eth(
+ memo::coin_purchase_memo::EthAddressDerivation {
+ keypath: vec![
+ 44 + util::bip32::HARDENED,
+ 60 + util::bip32::HARDENED,
+ 0,
+ ],
+ },
+ )),
+ )],
+ nonce: vec![],
+ total_amount: 25000000,
+ signature: vec![],
+ },
+ // Display amount must contain both numeric amount and destination unit.
+ pb::BtcPaymentRequestRequest {
+ recipient_name: "SWAPKIT (Provider)".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "ETH",
+ "0x123",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ )],
+ nonce: vec![],
+ total_amount: 25000000,
+ signature: vec![],
+ },
+ // Display amount must be exactly "<positive-decimal> <unit>".
+ pb::BtcPaymentRequestRequest {
+ recipient_name: "SWAPKIT (Provider)".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "foo bar baz",
+ "0x123",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ )],
+ nonce: vec![],
+ total_amount: 25000000,
+ signature: vec![],
+ },
+ ] {
+ let mut mock_hal = TestingHal::new();
+ assert_eq!(
+ block_on(user_verify(
+ &mut mock_hal,
+ coin_params,
+ &payment_request,
+ FormatUnit::Default,
+ )),
+ Err(Error::InvalidInput)
+ );
+ }
+ }
}
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
index 7a89b8e..38e6c58 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
@@ -192,6 +192,44 @@ async fn get_antiklepto_host_nonce(
}
}
+/// Validates swap-specific source account constraints after a CoinPurchaseMemo was detected.
+/// Coin must be BTC/LTC and all selected source configs must be single-sig.
+#[cfg(feature = "app-ethereum")]
+fn validate_swap_source_account(
+ coin: pb::BtcCoin,
+ script_configs: &[ValidatedScriptConfigWithKeypath],
+) -> Result<(), Error> {
+ match coin {
+ pb::BtcCoin::Btc | pb::BtcCoin::Ltc => {}
+ _ => return Err(Error::InvalidInput),
+ }
+
+ if script_configs.is_empty() {
+ return Err(Error::InvalidInput);
+ }
+
+ for script_config in script_configs {
+ match script_config {
+ ValidatedScriptConfigWithKeypath {
+ config: ValidatedScriptConfig::SimpleType(_),
+ ..
+ } => {}
+ _ => return Err(Error::InvalidInput),
+ }
+ }
+
+ Ok(())
+}
+
+/// CoinPurchaseMemo-backed swaps require Ethereum support.
+#[cfg(not(feature = "app-ethereum"))]
+fn validate_swap_source_account(
+ _coin: pb::BtcCoin,
+ _script_configs: &[ValidatedScriptConfigWithKeypath],
+) -> Result<(), Error> {
+ Err(Error::Disabled)
+}
+
fn validate_keypath(
params: &super::params::Params,
script_config_account: &ValidatedScriptConfigWithKeypath,
@@ -961,6 +999,9 @@ async fn _process(
}
let payment_request: pb::BtcPaymentRequestRequest =
get_payment_request(output_payment_request_index, &mut next_response).await?;
+ if payment_request::contains_coin_purchase_memo(&payment_request) {
+ validate_swap_source_account(coin, &validated_script_configs)?;
+ }
payment_request::user_verify(hal, coin_params, &payment_request, format_unit)
.await?;
match payment_request::validate(
@@ -3770,6 +3811,224 @@ mod tests {
);
}
+ #[cfg(feature = "app-ethereum")]
+ #[test]
+ pub fn test_validate_swap_source_account() {
+ // Swap payment requests are only supported for BTC/LTC source accounts,
+ // and only when the selected source config is simple single-sig.
+ let keypath = &[84 + HARDENED, 0 + HARDENED, 10 + HARDENED];
+ let multisig_pb = pb::btc_script_config::Multisig {
+ threshold: 1,
+ xpubs: vec![],
+ our_xpub_index: 0,
+ script_type: pb::btc_script_config::multisig::ScriptType::P2wsh as _,
+ };
+ let singlesig_account = [ValidatedScriptConfigWithKeypath {
+ keypath,
+ config: ValidatedScriptConfig::SimpleType(SimpleType::P2wpkh),
+ }];
+ let mixed_singlesig_accounts = [
+ ValidatedScriptConfigWithKeypath {
+ keypath,
+ config: ValidatedScriptConfig::SimpleType(SimpleType::P2wpkh),
+ },
+ ValidatedScriptConfigWithKeypath {
+ keypath,
+ config: ValidatedScriptConfig::SimpleType(SimpleType::P2tr),
+ },
+ ];
+ let multisig_account = [ValidatedScriptConfigWithKeypath {
+ keypath,
+ config: ValidatedScriptConfig::Multisig {
+ name: "test multisig".into(),
+ multisig: &multisig_pb,
+ },
+ }];
+ let mixed_accounts = [
+ ValidatedScriptConfigWithKeypath {
+ keypath,
+ config: ValidatedScriptConfig::SimpleType(SimpleType::P2wpkh),
+ },
+ ValidatedScriptConfigWithKeypath {
+ keypath,
+ config: ValidatedScriptConfig::Multisig {
+ name: "test multisig".into(),
+ multisig: &multisig_pb,
+ },
+ },
+ ];
+
+ assert_eq!(
+ validate_swap_source_account(pb::BtcCoin::Btc, &singlesig_account),
+ Ok(())
+ );
+ assert_eq!(
+ validate_swap_source_account(pb::BtcCoin::Ltc, &singlesig_account),
+ Ok(())
+ );
+ assert_eq!(
+ validate_swap_source_account(pb::BtcCoin::Btc, &mixed_singlesig_accounts),
+ Ok(())
+ );
+ assert_eq!(
+ validate_swap_source_account(pb::BtcCoin::Tbtc, &singlesig_account),
+ Err(Error::InvalidInput)
+ );
+ assert_eq!(
+ validate_swap_source_account(pb::BtcCoin::Btc, &multisig_account),
+ Err(Error::InvalidInput)
+ );
+ assert_eq!(
+ validate_swap_source_account(pb::BtcCoin::Btc, &mixed_accounts),
+ Err(Error::InvalidInput)
+ );
+ }
+
+ #[cfg(feature = "app-ethereum")]
+ #[test]
+ pub fn test_swap_payment_request() {
+ // End-to-end swap signing: swap screens appear, then the regular BTC confirmations continue.
+ let transaction =
+ alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
+
+ {
+ let mut tx = transaction.borrow_mut();
+ tx.total_confirmations += 1;
+ let payment_request_output_index = 1;
+ let output_value = tx.outputs[payment_request_output_index].value;
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![Memo {
+ memo: Some(memo::Memo::CoinPurchaseMemo(memo::CoinPurchaseMemo {
+ coin_type: 60,
+ amount: "0.25 ETH".into(),
+ address: "0x773A77b9D32589be03f9132AF759e294f7851be9".into(),
+ address_derivation: Some(memo::coin_purchase_memo::AddressDerivation::Eth(
+ memo::coin_purchase_memo::EthAddressDerivation {
+ keypath: vec![44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0],
+ },
+ )),
+ })),
+ }],
+ nonce: vec![],
+ total_amount: output_value,
+ signature: vec![],
+ };
+ let coin_params = super::super::params::get(tx.coin);
+ payment_request::tst_sign_payment_request(
+ coin_params,
+ &mut payment_request,
+ output_value,
+ "34oVnh4gNviJGMnNvgquMeLAxvXJuaRVMZ",
+ );
+ tx.payment_request = Some(payment_request);
+ tx.outputs[payment_request_output_index].payment_request_index = Some(0);
+ }
+
+ mock_host_responder(transaction.clone());
+ let init_request = transaction.borrow().init_request();
+
+ let mut mock_hal = TestingHal::new();
+ let result = block_on(process(&mut mock_hal, &init_request));
+ assert!(result.is_ok());
+
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![
+ Screen::Recipient {
+ recipient: "12ZE w5Hc v1hT b6YU QJ69 y1V7 uhco Dz92 PH".into(),
+ amount: "1.00000000 BTC".into(),
+ },
+ Screen::Recipient {
+ recipient: "Test Merchant".into(),
+ amount: "12.34567890 BTC".into(),
+ },
+ Screen::Confirm {
+ title: "SWAP".into(),
+ body: "12.34567890 BTC\nto\n0.25 ETH".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Receive to".into(),
+ body: "ETH account #1".into(),
+ longtouch: false,
+ },
+ Screen::Recipient {
+ recipient: "bc1q xven xven xven xven xven xven xven xven 2ymj t8".into(),
+ amount: "0.00006000 BTC".into(),
+ },
+ Screen::Recipient {
+ recipient: "bc1q g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zq d8sx w4".into(),
+ amount: "0.00007000 BTC".into(),
+ },
+ Screen::Confirm {
+ title: "Warning".into(),
+ body: "There are 2\nchange outputs.\nProceed?".into(),
+ longtouch: false,
+ },
+ Screen::TotalFee {
+ total: "13.39999900 BTC".into(),
+ fee: "0.05419010 BTC".into(),
+ longtouch: true,
+ },
+ Screen::Status {
+ title: "Transaction\nconfirmed".into(),
+ success: true
+ },
+ ]
+ );
+ }
+
+ #[cfg(feature = "app-ethereum")]
+ #[test]
+ pub fn test_swap_payment_request_unsupported_source_coin() {
+ // Swap UI is restricted to BTC/LTC source accounts; other BTC-like coins must fail early.
+ let transaction = alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(
+ pb::BtcCoin::Tbtc,
+ )));
+
+ {
+ let mut tx = transaction.borrow_mut();
+ let payment_request_output_index = 1;
+ let output_value = tx.outputs[payment_request_output_index].value;
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![Memo {
+ memo: Some(memo::Memo::CoinPurchaseMemo(memo::CoinPurchaseMemo {
+ coin_type: 60,
+ amount: "0.25 ETH".into(),
+ address: "0x773A77b9D32589be03f9132AF759e294f7851be9".into(),
+ address_derivation: Some(memo::coin_purchase_memo::AddressDerivation::Eth(
+ memo::coin_purchase_memo::EthAddressDerivation {
+ keypath: vec![44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0],
+ },
+ )),
+ })),
+ }],
+ nonce: vec![],
+ total_amount: output_value,
+ signature: vec![],
+ };
+ let coin_params = super::super::params::get(tx.coin);
+ payment_request::tst_sign_payment_request(
+ coin_params,
+ &mut payment_request,
+ output_value,
+ "2MvdL2uD2Ubr4Zx8e6CQqNQEHjQ75sGH1NN",
+ );
+ tx.payment_request = Some(payment_request);
+ tx.outputs[payment_request_output_index].payment_request_index = Some(0);
+ }
+
+ mock_host_responder(transaction.clone());
+ let init_request = transaction.borrow().init_request();
+
+ assert_eq!(
+ block_on(process(&mut TestingHal::new(), &init_request)),
+ Err(Error::InvalidInput)
+ );
+ }
+
#[test]
fn test_op_return() {
let transaction =
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.