From 1fe408dbd574f6b481650f5bf4455e31a6b5451f Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Mon, 4 May 2026 13:08:12 -0700 Subject: [PATCH] move core tx building and signing into the key-wallet-manager crate --- key-wallet-ffi/src/transaction.rs | 152 ++------ .../examples/wallet_creation.rs | 23 +- key-wallet-manager/src/error.rs | 7 + key-wallet-manager/src/event_tests.rs | 1 + key-wallet-manager/src/lib.rs | 337 ++---------------- key-wallet-manager/src/process_block.rs | 12 +- key-wallet-manager/tests/integration_test.rs | 34 +- .../src/wallet/managed_wallet_info/mod.rs | 93 ++++- .../transaction_building.rs | 115 +++++- 9 files changed, 285 insertions(+), 489 deletions(-) diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index 1eb1c28f6..d11ec34f0 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -1,10 +1,5 @@ //! Transaction building and management -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; -use std::ptr; -use std::slice; - use crate::error::{FFIError, FFIErrorCode}; use crate::types::{ transaction_context_from_ffi, FFIBlockInfo, FFITransactionContextType, FFIWallet, @@ -16,15 +11,16 @@ use dashcore::{ consensus, hashes::Hash, sighash::SighashCache, EcdsaSighashType, Network, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, Txid, }; -use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::wallet::managed_wallet_info::asset_lock_builder::{ AssetLockFundingType, CreditOutputFunding, }; use key_wallet::wallet::managed_wallet_info::fee::FeeRate; -use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; -use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use secp256k1::{Message, Secp256k1, SecretKey}; - +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; +use std::slice; +use std::str::FromStr; // MARK: - Transaction Types /// Opaque handle for a transaction @@ -111,128 +107,40 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( return false; } - unsafe { - use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; - use key_wallet::wallet::managed_wallet_info::transaction_builder::TransactionBuilder; - let network_rust = wallet_ref.inner().network; - let outputs_slice = slice::from_raw_parts(outputs, outputs_count); + let ffi_outputs = slice::from_raw_parts(outputs, outputs_count); + let mut outputs = Vec::with_capacity(outputs_count); + for output in ffi_outputs { + if output.address.is_null() { + (*error).set(FFIErrorCode::InvalidInput, "Output address pointer is null"); + return false; + } + + // Convert address from C string + let address_str = unwrap_or_return!(CStr::from_ptr(output.address).to_str(), error); + + // Parse address using dashcore + let address = unwrap_or_return!(dashcore::Address::from_str(address_str), error); + + outputs.push((address, output.amount)); + } + + let wallet_id = wallet_ref.inner().wallet_id; + + unsafe { manager_ref.runtime.block_on(async { let mut manager = manager_ref.manager.write().await; - let wallet_id = wallet_ref.inner().wallet_id; - // Get change address through the manager - let result = unwrap_or_return!( - manager.get_change_address( + let (transaction, fee) = unwrap_or_return!( + manager.build_and_sign_transaction( &wallet_id, account_index, - AccountTypePreference::BIP44, - true, + outputs, + FeeRate::new(fee_per_kb) ), error ); - let change_address = unwrap_or_return!(result.address, error); - - // Get the managed account for UTXOs and signing data - let managed_wallet = unwrap_or_return!(manager.get_wallet_info_mut(&wallet_id), error); - - let managed_account = unwrap_or_return!( - managed_wallet.accounts.standard_bip44_accounts.get_mut(&account_index), - error - ); - - // Convert FFI outputs to Rust outputs - let mut tx_builder = TransactionBuilder::new(); - - for output in outputs_slice { - if output.address.is_null() { - (*error).set(FFIErrorCode::InvalidInput, "Output address pointer is null"); - return false; - } - - // Convert address from C string - let address_str = unwrap_or_return!(CStr::from_ptr(output.address).to_str(), error); - - // Parse address using dashcore - use std::str::FromStr; - let parsed = unwrap_or_return!(dashcore::Address::from_str(address_str), error); - let address = unwrap_or_return!(parsed.require_network(network_rust), error); - - // Add output - tx_builder = - unwrap_or_return!(tx_builder.add_output(&address, output.amount), error); - } - - tx_builder = tx_builder - .set_change_address(change_address) - .set_fee_rate(FeeRate::new(fee_per_kb)); - - // Get available UTXOs (collect owned UTXOs, not references) - let utxos: Vec = managed_account.utxos.values().cloned().collect(); - - // Get the wallet's root extended private key for signing - use key_wallet::wallet::WalletType; - - let root_xpriv = match &wallet_ref.inner().wallet_type { - WalletType::Mnemonic { - root_extended_private_key, - .. - } => root_extended_private_key, - WalletType::Seed { - root_extended_private_key, - .. - } => root_extended_private_key, - WalletType::ExtendedPrivKey(root_extended_private_key) => root_extended_private_key, - _ => { - (*error).set(FFIErrorCode::WalletError, "Cannot sign with watch-only wallet"); - return false; - } - }; - - // Build a map of address -> derivation path for all addresses in the account - use std::collections::HashMap; - let mut address_to_path: HashMap = - HashMap::new(); - - // Collect from all address pools (receive, change, etc.) - for pool in managed_account.managed_account_type().address_pools() { - for addr_info in pool.addresses.values() { - address_to_path.insert(addr_info.address.clone(), addr_info.path.clone()); - } - } - - // Select inputs and build transaction - let select_inputs_result = tx_builder.select_inputs( - &utxos, - SelectionStrategy::BranchAndBound, - managed_wallet.last_processed_height(), - |utxo| { - // Look up the derivation path for this UTXO's address - let path = address_to_path.get(&utxo.address)?; - - // Convert root key to ExtendedPrivKey and derive the child key - let root_ext_priv = root_xpriv.to_extended_priv_key(network_rust); - let secp = secp256k1::Secp256k1::new(); - let derived_xpriv = root_ext_priv.derive_priv(&secp, path).ok()?; - - Some(derived_xpriv.private_key) - }, - ); - let mut tx_builder_with_inputs = unwrap_or_return!(select_inputs_result, error); - let transaction = unwrap_or_return!(tx_builder_with_inputs.build(), error); - - // This is tricky, the transaction creation + fee calculation need a little - // bit of love to avoid this kind of logic. - // - // First, we need to know that TransactionBuilder may add an extra output for change - // to the final transaction but not to itself, with that knowledge, we can compare the - // number of outputs in the transaction with the number of outputs in the TransactionBuilder - // to then call the appropriate fee calculation method - *fee_out = if transaction.output.len() > tx_builder_with_inputs.outputs().len() { - tx_builder_with_inputs.calculate_fee_with_extra_output() - } else { - tx_builder_with_inputs.calculate_fee() - }; + *fee_out = fee; // Serialize the transaction let serialized = consensus::serialize(&transaction); diff --git a/key-wallet-manager/examples/wallet_creation.rs b/key-wallet-manager/examples/wallet_creation.rs index fd82448b8..beb3f3160 100644 --- a/key-wallet-manager/examples/wallet_creation.rs +++ b/key-wallet-manager/examples/wallet_creation.rs @@ -90,30 +90,17 @@ fn main() { // Example 4: Generate addresses println!("\n4. Generating addresses..."); - // Note: This might fail with InvalidNetwork error if the account collection - // isn't properly initialized in the managed wallet info - let address_result = manager.get_receive_address( + let address = manager.next_receive_address( &wallet_id, 0, // Account index AccountTypePreference::BIP44, false, // Don't advance index ); - match address_result { - Ok(result) => { - if let Some(address) = result.address { - println!("✅ Receive address: {}", address); - if let Some(account_type) = result.account_type_used { - println!(" Account type used: {:?}", account_type); - } - } else { - println!("⚠️ No address generated"); - } - } - Err(e) => { - println!("⚠️ Could not get address: {:?}", e); - println!(" (This is expected with the current implementation)"); - } + if let Some(address) = address { + println!("✅ Receive address: {}", address); + } else { + println!("⚠️ No address generated"); } // Example 5: WalletManager now includes SPV functionality diff --git a/key-wallet-manager/src/error.rs b/key-wallet-manager/src/error.rs index 9f96b7e2c..e4dbd35e1 100644 --- a/key-wallet-manager/src/error.rs +++ b/key-wallet-manager/src/error.rs @@ -1,6 +1,7 @@ //! Error types for the wallet manager. use crate::WalletId; +use key_wallet::wallet::managed_wallet_info::transaction_builder::BuilderError; /// Wallet manager errors #[derive(Debug)] @@ -97,3 +98,9 @@ impl From for WalletError { } } } + +impl From for WalletError { + fn from(err: BuilderError) -> Self { + WalletError::TransactionBuild(format!("Transaction building error: {}", err)) + } +} diff --git a/key-wallet-manager/src/event_tests.rs b/key-wallet-manager/src/event_tests.rs index 37f2752a3..beb48fa95 100644 --- a/key-wallet-manager/src/event_tests.rs +++ b/key-wallet-manager/src/event_tests.rs @@ -15,6 +15,7 @@ use dashcore::{ }; use key_wallet::account::StandardAccountType; use key_wallet::managed_account::address_pool::AddressPoolType; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::managed_account_type::ManagedAccountType; use key_wallet::AccountType; use std::collections::BTreeSet; diff --git a/key-wallet-manager/src/lib.rs b/key-wallet-manager/src/lib.rs index e2db38420..b0be0024e 100644 --- a/key-wallet-manager/src/lib.rs +++ b/key-wallet-manager/src/lib.rs @@ -27,7 +27,6 @@ pub use wallet_interface::{BlockProcessingResult, MempoolTransactionResult, Wall use dashcore::blockdata::transaction::Transaction; use dashcore::prelude::CoreBlockHeight; use key_wallet::account::AccountCollection; -use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::transaction_record::TransactionRecord; use key_wallet::transaction_checking::{DerivedAddressInfo, TransactionContext}; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; @@ -38,6 +37,8 @@ use key_wallet::{ExtendedPubKey, WalletCoreBalance}; use std::collections::{BTreeMap, BTreeSet}; use std::str::FromStr; +use dashcore::address::NetworkUnchecked; +use key_wallet::wallet::managed_wallet_info::fee::FeeRate; use tokio::sync::broadcast; /// Default capacity for the wallet event bus. @@ -570,334 +571,52 @@ impl WalletManager { self.bump_structural_revision(); Ok(()) } +} +impl WalletManager { /// Get receive address from a specific wallet and account - pub fn get_receive_address( + pub fn next_receive_address( &mut self, wallet_id: &WalletId, account_index: u32, account_type_pref: AccountTypePreference, mark_as_used: bool, - ) -> Result { + ) -> Option
{ // Get the wallet account to access the xpub - let wallet = self.wallets.get(wallet_id).ok_or(WalletError::WalletNotFound(*wallet_id))?; - - let managed_info = - self.wallet_infos.get_mut(wallet_id).ok_or(WalletError::WalletNotFound(*wallet_id))?; - - // Get the account collection for the network - let collection = managed_info.accounts_mut(); - - // Try to get address based on preference - let (address_opt, account_type_used) = match account_type_pref { - AccountTypePreference::BIP44 => { - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip44_accounts.get_mut(&account_index), - wallet.get_bip44_account(account_index), - ) { - match managed_account - .next_receive_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP44)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - AccountTypePreference::BIP32 => { - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip32_accounts.get_mut(&account_index), - wallet.get_bip32_account(account_index), - ) { - match managed_account - .next_receive_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP32)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - AccountTypePreference::PreferBIP44 => { - // Try BIP44 first - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip44_accounts.get_mut(&account_index), - wallet.get_bip44_account(account_index), - ) { - match managed_account - .next_receive_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP44)), - Err(_) => { - // Fallback to BIP32 - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip32_accounts.get_mut(&account_index), - wallet.get_bip32_account(account_index), - ) { - match managed_account - .next_receive_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP32)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - } - } else if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip32_accounts.get_mut(&account_index), - wallet.get_bip32_account(account_index), - ) { - match managed_account - .next_receive_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP32)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - AccountTypePreference::PreferBIP32 => { - // Try BIP32 first - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip32_accounts.get_mut(&account_index), - wallet.get_bip32_account(account_index), - ) { - match managed_account - .next_receive_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP32)), - Err(_) => { - // Fallback to BIP44 - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip44_accounts.get_mut(&account_index), - wallet.get_bip44_account(account_index), - ) { - match managed_account - .next_receive_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP44)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - } - } else if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip44_accounts.get_mut(&account_index), - wallet.get_bip44_account(account_index), - ) { - match managed_account - .next_receive_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP44)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - }; + let (wallet, managed_info) = self.get_wallet_and_info_mut(wallet_id)?; - // Mark the address as used if requested - if let Some(ref address) = address_opt { - if mark_as_used { - // Get the account collection again for marking - let collection = managed_info.accounts_mut(); - // Mark address as used in the appropriate account type - match account_type_used { - Some(AccountTypeUsed::BIP44) => { - if let Some(account) = - collection.standard_bip44_accounts.get_mut(&account_index) - { - account.mark_address_used(address); - } - } - Some(AccountTypeUsed::BIP32) => { - if let Some(account) = - collection.standard_bip32_accounts.get_mut(&account_index) - { - account.mark_address_used(address); - } - } - None => {} - } - } - } - - Ok(AddressGenerationResult { - address: address_opt, - account_type_used, - }) + managed_info.next_receive_address(wallet, account_index, account_type_pref, mark_as_used) } /// Get change address from a specific wallet and account - pub fn get_change_address( + pub fn next_change_address( &mut self, wallet_id: &WalletId, account_index: u32, account_type_pref: AccountTypePreference, mark_as_used: bool, - ) -> Result { + ) -> Option
{ // Get the wallet account to access the xpub - let wallet = self.wallets.get(wallet_id).ok_or(WalletError::WalletNotFound(*wallet_id))?; - let managed_info = - self.wallet_infos.get_mut(wallet_id).ok_or(WalletError::WalletNotFound(*wallet_id))?; - - // Get the account collection for the network - let collection = managed_info.accounts_mut(); - - // Try to get address based on preference - let (address_opt, account_type_used) = match account_type_pref { - AccountTypePreference::BIP44 => { - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip44_accounts.get_mut(&account_index), - wallet.get_bip44_account(account_index), - ) { - match managed_account - .next_change_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP44)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - AccountTypePreference::BIP32 => { - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip32_accounts.get_mut(&account_index), - wallet.get_bip32_account(account_index), - ) { - match managed_account - .next_change_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP32)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - AccountTypePreference::PreferBIP44 => { - // Try BIP44 first - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip44_accounts.get_mut(&account_index), - wallet.get_bip44_account(account_index), - ) { - match managed_account - .next_change_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP44)), - Err(_) => { - // Fallback to BIP32 - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip32_accounts.get_mut(&account_index), - wallet.get_bip32_account(account_index), - ) { - match managed_account - .next_change_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP32)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - } - } else if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip32_accounts.get_mut(&account_index), - wallet.get_bip32_account(account_index), - ) { - match managed_account - .next_change_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP32)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - AccountTypePreference::PreferBIP32 => { - // Try BIP32 first - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip32_accounts.get_mut(&account_index), - wallet.get_bip32_account(account_index), - ) { - match managed_account - .next_change_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP32)), - Err(_) => { - // Fallback to BIP44 - if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip44_accounts.get_mut(&account_index), - wallet.get_bip44_account(account_index), - ) { - match managed_account - .next_change_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP44)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - } - } else if let (Some(managed_account), Some(wallet_account)) = ( - collection.standard_bip44_accounts.get_mut(&account_index), - wallet.get_bip44_account(account_index), - ) { - match managed_account - .next_change_address(Some(&wallet_account.account_xpub), true) - { - Ok(addr) => (Some(addr), Some(AccountTypeUsed::BIP44)), - Err(_) => (None, None), - } - } else { - (None, None) - } - } - }; + let (wallet, managed_info) = self.get_wallet_and_info_mut(wallet_id)?; - // Mark the address as used if requested - if let Some(ref address) = address_opt { - if mark_as_used { - // Get the account collection again for marking - let collection = managed_info.accounts_mut(); - // Mark address as used in the appropriate account type - match account_type_used { - Some(AccountTypeUsed::BIP44) => { - if let Some(account) = - collection.standard_bip44_accounts.get_mut(&account_index) - { - account.mark_address_used(address); - } - } - Some(AccountTypeUsed::BIP32) => { - if let Some(account) = - collection.standard_bip32_accounts.get_mut(&account_index) - { - account.mark_address_used(address); - } - } - None => {} - } - } - } + managed_info.next_change_address(wallet, account_index, account_type_pref, mark_as_used) + } - Ok(AddressGenerationResult { - address: address_opt, - account_type_used, - }) + pub fn build_and_sign_transaction( + &mut self, + wallet_id: &WalletId, + account_index: u32, + outputs: Vec<(Address, u64)>, + fee_rate: FeeRate, + ) -> Result<(Transaction, u64), WalletError> { + // Get the managed account for UTXOs and signing data + let (wallet, managed_wallet) = self + .get_wallet_and_info_mut(wallet_id) + .ok_or(WalletError::WalletNotFound(*wallet_id))?; + + managed_wallet + .build_and_sign_transaction(wallet, account_index, outputs, fee_rate) + .map_err(|e| WalletError::TransactionBuild(e.to_string())) } } diff --git a/key-wallet-manager/src/process_block.rs b/key-wallet-manager/src/process_block.rs index f4e818984..aac4c16f0 100644 --- a/key-wallet-manager/src/process_block.rs +++ b/key-wallet-manager/src/process_block.rs @@ -681,17 +681,17 @@ mod tests { assert_eq!(manager.monitor_revision(), expected_rev, "after create_account"); // get_receive_address bumps (when address is generated) - let result = - manager.get_receive_address(&wallet_id, 0, AccountTypePreference::PreferBIP44, true); - if result.is_ok() && result.unwrap().address.is_some() { + let address = + manager.next_receive_address(&wallet_id, 0, AccountTypePreference::BIP44, true); + if address.is_some() { expected_rev += 1; assert_eq!(manager.monitor_revision(), expected_rev, "after get_receive_address"); } // get_change_address bumps (when address is generated) - let result = - manager.get_change_address(&wallet_id, 0, AccountTypePreference::PreferBIP44, true); - if result.is_ok() && result.unwrap().address.is_some() { + let address = + manager.next_change_address(&wallet_id, 0, AccountTypePreference::BIP44, true); + if address.is_some() { expected_rev += 1; assert_eq!(manager.monitor_revision(), expected_rev, "after get_change_address"); } diff --git a/key-wallet-manager/tests/integration_test.rs b/key-wallet-manager/tests/integration_test.rs index 43a5e8b34..2eb01b3f3 100644 --- a/key-wallet-manager/tests/integration_test.rs +++ b/key-wallet-manager/tests/integration_test.rs @@ -9,7 +9,7 @@ use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoIn use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::{mnemonic::Language, Mnemonic, Network}; use key_wallet_manager::WalletInterface; -use key_wallet_manager::{WalletError, WalletManager}; +use key_wallet_manager::WalletManager; #[test] fn test_wallet_manager_creation() { @@ -82,31 +82,13 @@ fn test_address_generation() { let wallet_id = wallet_result.unwrap(); // The wallet should already have account 0 from creation - // But the managed wallet info might not have the account collection initialized - - // Test address generation - it may fail if accounts aren't initialized - let address1 = manager.get_receive_address(&wallet_id, 0, AccountTypePreference::BIP44, false); - // This might fail with InvalidNetwork if the account collection isn't initialized - // We'll check if it's the expected error - if let Err(ref e) = address1 { - match e { - WalletError::InvalidNetwork => { - // This is expected given the current implementation - // The managed wallet info doesn't initialize account collections - return; - } - _ => panic!("Unexpected error: {:?}", e), - } - } - - let change = manager.get_change_address(&wallet_id, 0, AccountTypePreference::BIP44, false); - // Same check for change address - if let Err(ref e) = change { - match e { - WalletError::InvalidNetwork => {} - _ => panic!("Unexpected error: {:?}", e), - } - } + + // Test address generation - it may be missing if accounts aren't initialized + let address1 = manager.next_receive_address(&wallet_id, 0, AccountTypePreference::BIP44, false); + assert!(address1.is_some(), "Failed to generate receive address: {:?}", address1); + + let change = manager.next_change_address(&wallet_id, 0, AccountTypePreference::BIP44, false); + assert!(change.is_some(), "Failed to generate change: {:?}", change); } #[test] diff --git a/key-wallet/src/wallet/managed_wallet_info/mod.rs b/key-wallet/src/wallet/managed_wallet_info/mod.rs index ff583752b..d062160b7 100644 --- a/key-wallet/src/wallet/managed_wallet_info/mod.rs +++ b/key-wallet/src/wallet/managed_wallet_info/mod.rs @@ -18,9 +18,12 @@ pub use managed_account_operations::ManagedAccountOperations; use super::balance::WalletCoreBalance; use super::metadata::WalletMetadata; use crate::account::ManagedAccountCollection; -use crate::Network; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; +use crate::wallet::managed_wallet_info::transaction_building::AccountTypePreference; +use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; +use crate::{Network, Wallet}; use dashcore::prelude::CoreBlockHeight; -use dashcore::Txid; +use dashcore::{Address, Txid}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -132,6 +135,92 @@ impl ManagedWalletInfo { pub fn instant_send_locks(&self) -> &HashSet { &self.instant_send_locks } + + pub fn next_change_address( + &mut self, + wallet: &Wallet, + account_index: u32, + account_type_pref: AccountTypePreference, + mark_as_used: bool, + ) -> Option
{ + let collection = self.accounts_mut(); + + let address = match account_type_pref { + AccountTypePreference::BIP44 => { + let managed_account = collection.standard_bip44_accounts.get_mut(&account_index)?; + let wallet_account = wallet.get_bip44_account(account_index)?; + + let address = managed_account + .next_change_address(Some(&wallet_account.account_xpub), true) + .ok(); + + if let (Some(address), true) = (&address, mark_as_used) { + managed_account.mark_address_used(address); + } + + address + } + AccountTypePreference::BIP32 => { + let managed_account = collection.standard_bip32_accounts.get_mut(&account_index)?; + let wallet_account = wallet.get_bip32_account(account_index)?; + + let address = managed_account + .next_change_address(Some(&wallet_account.account_xpub), true) + .ok(); + + if let (Some(address), true) = (&address, mark_as_used) { + managed_account.mark_address_used(address); + } + + address + } + }; + + address + } + + pub fn next_receive_address( + &mut self, + wallet: &Wallet, + account_index: u32, + account_type_pref: AccountTypePreference, + mark_as_used: bool, + ) -> Option
{ + let collection = self.accounts_mut(); + + let address = match account_type_pref { + AccountTypePreference::BIP44 => { + let managed_account = collection.standard_bip44_accounts.get_mut(&account_index)?; + let wallet_account = wallet.get_bip44_account(account_index)?; + + let address = managed_account + .next_receive_address(Some(&wallet_account.account_xpub), true) + .ok(); + + if let (Some(address), true) = (&address, mark_as_used) { + managed_account.mark_address_used(address); + } + + address + } + AccountTypePreference::BIP32 => { + let managed_account = collection.standard_bip32_accounts.get_mut(&account_index)?; + let wallet_account = wallet.get_bip32_account(account_index)?; + + let address = managed_account + .next_receive_address(Some(&wallet_account.account_xpub), true) + .ok(); + + if let (Some(address), true) = (&address, mark_as_used) { + managed_account.mark_address_used(address); + } + + address + } + }; + + address + } } /// Re-export types from account module for convenience diff --git a/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs b/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs index 96072a7df..5c2e8c62b 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs @@ -1,18 +1,121 @@ //! Transaction building functionality for managed wallets +use crate::managed_account::managed_account_trait::ManagedAccountTrait; +use crate::wallet::managed_wallet_info::coin_selection::SelectionStrategy; +use crate::wallet::managed_wallet_info::fee::FeeRate; +use crate::wallet::managed_wallet_info::transaction_builder::{BuilderError, TransactionBuilder}; +use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; +use crate::wallet::{ManagedWalletInfo, WalletType}; +use crate::Wallet; +use dashcore::address::NetworkUnchecked; +use dashcore::{Address, Transaction}; + /// Account type preference for transaction building #[derive(Debug, Clone, Copy)] pub enum AccountTypePreference { - /// Use BIP44 account only BIP44, - /// Use BIP32 account only BIP32, - /// Prefer BIP44, fallback to BIP32 - PreferBIP44, - /// Prefer BIP32, fallback to BIP44 - PreferBIP32, } +impl ManagedWalletInfo { + pub fn build_and_sign_transaction( + &mut self, + wallet: &Wallet, + account_index: u32, + outputs: Vec<(Address, u64)>, + fee_rate: FeeRate, + ) -> Result<(Transaction, u64), BuilderError> { + // Get change address through the manager + let change_address = self + .next_change_address(wallet, account_index, AccountTypePreference::BIP44, true) + .ok_or(BuilderError::NoChangeAddress)?; + + let managed_account = self + .accounts + .standard_bip44_accounts + .get_mut(&account_index) + .expect("Impossible state, if change address is Some, account must be Some"); + + // Convert FFI outputs to Rust outputs + let mut tx_builder = TransactionBuilder::new(); + + for output in outputs { + let checked_address = output.0.require_network(wallet.network).map_err(|e| { + BuilderError::InvalidData(format!("Output address network mismatch: {}", e)) + })?; + tx_builder = tx_builder.add_output(&checked_address, output.1)?; + } + + tx_builder = tx_builder.set_change_address(change_address).set_fee_rate(fee_rate); + + // Get available UTXOs (collect owned UTXOs, not references) + let utxos: Vec = managed_account.utxos.values().cloned().collect(); + + // Get the wallet's root extended private key for signing + let root_xpriv = match &wallet.wallet_type { + WalletType::Mnemonic { + root_extended_private_key, + .. + } => root_extended_private_key, + WalletType::Seed { + root_extended_private_key, + .. + } => root_extended_private_key, + WalletType::ExtendedPrivKey(root_extended_private_key) => root_extended_private_key, + _ => { + return Err(BuilderError::InvalidData( + "Cannot sign with watch-only wallet".to_string(), + )); + } + }; + + // Build a map of address -> derivation path for all addresses in the account + use std::collections::HashMap; + let mut address_to_path: HashMap = HashMap::new(); + + // Collect from all address pools (receive, change, etc.) + for pool in managed_account.managed_account_type().address_pools() { + for addr_info in pool.addresses.values() { + address_to_path.insert(addr_info.address.clone(), addr_info.path.clone()); + } + } + + // Select inputs and build transaction + let mut tx_builder = tx_builder.select_inputs( + &utxos, + SelectionStrategy::BranchAndBound, + self.last_processed_height(), + |utxo| { + // Look up the derivation path for this UTXO's address + let path = address_to_path.get(&utxo.address)?; + + // Convert root key to ExtendedPrivKey and derive the child key + let root_ext_priv = root_xpriv.to_extended_priv_key(wallet.network); + let secp = secp256k1::Secp256k1::new(); + let derived_xpriv = root_ext_priv.derive_priv(&secp, path).ok()?; + + Some(derived_xpriv.private_key) + }, + )?; + + let transaction = tx_builder.build()?; + + // This is tricky, the transaction creation + fee calculation need a little + // bit of love to avoid this kind of logic. + // + // First, we need to know that TransactionBuilder may add an extra output for change + // to the final transaction but not to itself, with that knowledge, we can compare the + // number of outputs in the transaction with the number of outputs in the TransactionBuilder + // to then call the appropriate fee calculation method + let fee = if transaction.output.len() > tx_builder.outputs().len() { + tx_builder.calculate_fee_with_extra_output() + } else { + tx_builder.calculate_fee() + }; + + Ok((transaction, fee)) + } +} #[cfg(test)] mod tests { use crate::wallet::managed_wallet_info::coin_selection::SelectionStrategy;