diff --git a/common/src/chain/transaction/signed_transaction_intent.rs b/common/src/chain/transaction/signed_transaction_intent.rs index 6b68536dc..9285393d0 100644 --- a/common/src/chain/transaction/signed_transaction_intent.rs +++ b/common/src/chain/transaction/signed_transaction_intent.rs @@ -66,6 +66,13 @@ pub struct SignedTransactionIntent { } impl SignedTransactionIntent { + pub fn new_unchecked(signed_message: String, signatures: Vec>) -> Self { + Self { + signed_message, + signatures, + } + } + /// Create a signed intent given the id of the transaction and its input destinations. /// /// Only PublicKeyHash and PublicKey destinations are supported by this function. diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 082851319..35b342c30 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -45,3 +45,5 @@ tempfile.workspace = true [features] trezor = ["dep:trezor-client", "wallet-types/trezor"] trezor-emulator = [] + +default = ["trezor"] diff --git a/wallet/src/signer/software_signer/tests.rs b/wallet/src/signer/software_signer/tests.rs index 2ba09d800..96838e610 100644 --- a/wallet/src/signer/software_signer/tests.rs +++ b/wallet/src/signer/software_signer/tests.rs @@ -83,6 +83,74 @@ fn sign_message(#[case] seed: Seed) { res.verify_signature(&config, &destination, &message_challenge).unwrap(); } +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn sign_transaction_intent(#[case] seed: Seed) { + use common::primitives::Idable; + + let mut rng = make_seedable_rng(seed); + + let config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let master_key_chain = MasterKeyChain::new_from_mnemonic( + config.clone(), + &mut db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) + .unwrap(); + let mut account = Account::new(config.clone(), &mut db_tx, key_chain, None).unwrap(); + + let mut signer = SoftwareSigner::new(config.clone(), DEFAULT_ACCOUNT_INDEX); + + let inputs: Vec = (0..rng.gen_range(1..5)) + .map(|_| { + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(&mut rng)).into() + } else { + Id::::new(H256::random_using(&mut rng)).into() + }; + TxInput::from_utxo(source_id, rng.next_u32()) + }) + .collect(); + let input_destinations: Vec<_> = (0..inputs.len()) + .map(|_| account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object()) + .collect(); + + let tx = Transaction::new( + 0, + inputs, + vec![TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(rng.gen())), + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), + )], + ) + .unwrap(); + + let intent: String = [rng.gen::(), rng.gen::(), rng.gen::()].iter().collect(); + let res = signer + .sign_transaction_intent( + &tx, + &input_destinations, + &intent, + account.key_chain(), + &db_tx, + ) + .unwrap(); + + let expected_signed_message = + SignedTransactionIntent::get_message_to_sign(&intent, &tx.get_id()); + res.verify(&config, &input_destinations, &expected_signed_message).unwrap(); +} + #[rstest] #[trace] #[case(Seed::from_entropy())] diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index 424eb31e2..6352c2ae8 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -44,7 +44,7 @@ use common::{ AccountCommand, AccountSpending, ChainConfig, Destination, OutPointSourceId, SignedTransactionIntent, Transaction, TxInput, TxOutput, }, - primitives::H256, + primitives::{Idable, H256}, }; use crypto::key::{ extended::ExtendedPublicKey, @@ -478,13 +478,25 @@ impl Signer for TrezorSigner { fn sign_transaction_intent( &mut self, - _transaction: &Transaction, - _input_destinations: &[Destination], - _intent: &str, - _key_chain: &impl AccountKeyChains, - _db_tx: &impl WalletStorageReadUnlocked, + transaction: &Transaction, + input_destinations: &[Destination], + intent: &str, + key_chain: &impl AccountKeyChains, + db_tx: &impl WalletStorageReadUnlocked, ) -> SignerResult { - unimplemented!("FIXME") + let tx_id = transaction.get_id(); + let message_to_sign = SignedTransactionIntent::get_message_to_sign(intent, &tx_id); + + let mut signatures = Vec::with_capacity(input_destinations.len()); + for dest in input_destinations { + let sig = self.sign_challenge(message_to_sign.as_bytes(), dest, key_chain, db_tx)?; + signatures.push(sig.into_raw()); + } + + Ok(SignedTransactionIntent::new_unchecked( + message_to_sign, + signatures, + )) } } diff --git a/wallet/src/signer/trezor_signer/tests.rs b/wallet/src/signer/trezor_signer/tests.rs index 26cd47d4c..ad3c9fad3 100644 --- a/wallet/src/signer/trezor_signer/tests.rs +++ b/wallet/src/signer/trezor_signer/tests.rs @@ -84,6 +84,78 @@ fn sign_message(#[case] seed: Seed) { res.verify_signature(&chain_config, &destination, &message_challenge).unwrap(); } +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn sign_transaction_intent(#[case] seed: Seed) { + use common::primitives::Idable; + + let mut rng = make_seedable_rng(seed); + + let config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let master_key_chain = MasterKeyChain::new_from_mnemonic( + config.clone(), + &mut db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) + .unwrap(); + let mut account = Account::new(config.clone(), &mut db_tx, key_chain, None).unwrap(); + + let mut devices = find_devices(false); + assert!(!devices.is_empty()); + let client = devices.pop().unwrap().connect().unwrap(); + + let mut signer = TrezorSigner::new(config.clone(), Arc::new(Mutex::new(client))); + + let inputs: Vec = (0..rng.gen_range(1..5)) + .map(|_| { + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(&mut rng)).into() + } else { + Id::::new(H256::random_using(&mut rng)).into() + }; + TxInput::from_utxo(source_id, rng.next_u32()) + }) + .collect(); + let input_destinations: Vec<_> = (0..inputs.len()) + .map(|_| account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object()) + .collect(); + + let tx = Transaction::new( + 0, + inputs, + vec![TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(rng.gen())), + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), + )], + ) + .unwrap(); + + let intent: String = [rng.gen::(), rng.gen::(), rng.gen::()].iter().collect(); + let res = signer + .sign_transaction_intent( + &tx, + &input_destinations, + &intent, + account.key_chain(), + &db_tx, + ) + .unwrap(); + + let expected_signed_message = + SignedTransactionIntent::get_message_to_sign(&intent, &tx.get_id()); + res.verify(&config, &input_destinations, &expected_signed_message).unwrap(); +} + #[rstest] #[trace] #[case(Seed::from_entropy())] diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index da22b16d1..03f29f888 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -1452,6 +1452,7 @@ where intent: String, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult<(SignedTransaction, SignedTransactionIntent)> { let (signed_tx, input_destinations) = self.create_transaction_to_addresses_impl( account_index, @@ -1461,7 +1462,7 @@ where current_fee_rate, consolidate_fee_rate, |send_request| send_request.destinations().to_owned(), - &BTreeMap::new(), // FIXME + additional_utxo_infos, )?; let signed_intent = self.for_account_rw_unlocked( diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index 107f14902..f883de2de 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -1213,6 +1213,7 @@ impl RuntimeWallet { intent: String, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult<(SignedTransaction, SignedTransactionIntent)> { match self { RuntimeWallet::Software(w) => w.create_transaction_to_addresses_with_intent( @@ -1223,6 +1224,7 @@ impl RuntimeWallet { intent, current_fee_rate, consolidate_fee_rate, + additional_utxo_infos, ), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.create_transaction_to_addresses_with_intent( @@ -1233,6 +1235,7 @@ impl RuntimeWallet { intent, current_fee_rate, consolidate_fee_rate, + additional_utxo_infos, ), } } diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index e5ed678af..bf4ab36c9 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -998,6 +998,13 @@ where account_index: U31, token_info: &UnconfirmedTokenInfo| { token_info.check_can_be_used()?; + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(token_info.token_id()), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: token_info.num_decimals(), + ticker: token_info.token_ticker().to_vec(), + }), + )]); wallet.create_transaction_to_addresses_with_intent( account_index, [output], @@ -1006,6 +1013,7 @@ where intent, current_fee_rate, consolidate_fee_rate, + &additional_info, ) }, )