diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index bf8e3e8c0..b835d4dd9 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -25,7 +25,7 @@ use key_wallet::managed_account::address_pool::AddressPool; use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::managed_platform_account::ManagedPlatformAccount; use key_wallet::managed_account::{ManagedCoreFundsAccount, ManagedCoreKeysAccount}; -use key_wallet::AccountType; +use key_wallet::{AccountType, ManagedAccountType}; /// Internal handle variant: a funds-bearing account or a keys-only account. pub(crate) enum FFIManagedCoreAccountInner { @@ -1285,9 +1285,13 @@ pub unsafe extern "C" fn managed_core_account_get_external_address_pool( let account = &*account; let managed_account = account.keys_account(); - // Get external address pool if this is a standard account + // External pool exists for the dual-pool account types (Standard and CoinJoin). match managed_account.managed_account_type() { - key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { + ManagedAccountType::Standard { + external_addresses, + .. + } + | ManagedAccountType::CoinJoin { external_addresses, .. } => { @@ -1321,9 +1325,14 @@ pub unsafe extern "C" fn managed_core_account_get_internal_address_pool( let account = &*account; let managed_account = account.keys_account(); - // Get internal address pool if this is a standard account + // Internal pool exists for the dual-pool account types (Standard and CoinJoin). + use key_wallet::managed_account::managed_account_type::ManagedAccountType; match managed_account.managed_account_type() { - key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { + ManagedAccountType::Standard { + internal_addresses, + .. + } + | ManagedAccountType::CoinJoin { internal_addresses, .. } => { @@ -1363,21 +1372,19 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( use key_wallet::managed_account::managed_account_type::ManagedAccountType; - match pool_type { + let pool = match pool_type { FFIAddressPoolType::External => { // Only standard accounts have external pools match managed_account.managed_account_type() { ManagedAccountType::Standard { external_addresses, .. - } => { - let ffi_pool = FFIAddressPool { - pool: external_addresses as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::External, - }; - Box::into_raw(Box::new(ffi_pool)) - } - _ => std::ptr::null_mut(), + } => external_addresses, + ManagedAccountType::CoinJoin { + external_addresses, + .. + } => external_addresses, + _ => return std::ptr::null_mut(), } } FFIAddressPoolType::Internal => { @@ -1386,19 +1393,17 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( ManagedAccountType::Standard { internal_addresses, .. - } => { - let ffi_pool = FFIAddressPool { - pool: internal_addresses as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::Internal, - }; - Box::into_raw(Box::new(ffi_pool)) - } - _ => std::ptr::null_mut(), + } => internal_addresses, + ManagedAccountType::CoinJoin { + internal_addresses, + .. + } => internal_addresses, + _ => return std::ptr::null_mut(), } } FFIAddressPoolType::Single => { // Get the single address pool for non-standard accounts - let pool_ref = match managed_account.managed_account_type() { + match managed_account.managed_account_type() { ManagedAccountType::Standard { .. } => { @@ -1406,9 +1411,11 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( return std::ptr::null_mut(); } ManagedAccountType::CoinJoin { - addresses, .. - } => addresses, + } => { + // Conjoin accounts don't have a "single" pool + return std::ptr::null_mut(); + } ManagedAccountType::IdentityRegistration { addresses, } => addresses, @@ -1452,15 +1459,15 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( addresses, .. } => addresses, - }; - - let ffi_pool = FFIAddressPool { - pool: pool_ref as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::Single, - }; - Box::into_raw(Box::new(ffi_pool)) + } } - } + }; + + let ffi_pool = FFIAddressPool { + pool: pool as *const AddressPool as *mut AddressPool, + pool_type, + }; + Box::into_raw(Box::new(ffi_pool)) } // ==================== Platform Payment Account Functions ==================== @@ -2256,20 +2263,19 @@ mod tests { let cj_account = cj_result.account; - // Test that external/internal return null for CoinJoin account let cj_external = managed_core_account_get_external_address_pool(cj_account); - assert!(cj_external.is_null()); + assert!(!cj_external.is_null()); let cj_internal = managed_core_account_get_internal_address_pool(cj_account); - assert!(cj_internal.is_null()); + assert!(!cj_internal.is_null()); - // Test that Single pool works for CoinJoin account let cj_single = managed_core_account_get_address_pool(cj_account, FFIAddressPoolType::Single); - assert!(!cj_single.is_null()); + assert!(cj_single.is_null()); // Clean up - address_pool_free(cj_single); + address_pool_free(cj_external); + address_pool_free(cj_internal); managed_core_account_free(cj_account); wallet_manager_free_wallet_ids(wallet_ids_out, count_out); wallet_manager_free(manager); diff --git a/key-wallet-ffi/src/utxo_tests.rs b/key-wallet-ffi/src/utxo_tests.rs index 74c0dddca..81fe55977 100644 --- a/key-wallet-ffi/src/utxo_tests.rs +++ b/key-wallet-ffi/src/utxo_tests.rs @@ -344,10 +344,16 @@ mod utxo_tests { let mut coinjoin_account = ManagedCoreFundsAccount::new( ManagedAccountType::CoinJoin { index: 0, - addresses: key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), + external_addresses: + key_wallet::managed_account::address_pool::AddressPoolBuilder::default() + .base_path(key_wallet::DerivationPath::from(vec![])) + .build() + .unwrap(), + internal_addresses: + key_wallet::managed_account::address_pool::AddressPoolBuilder::default() + .base_path(key_wallet::DerivationPath::from(vec![])) + .build() + .unwrap(), }, Network::Testnet, ); diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 8eaaac6e7..4c6c85ce1 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -585,20 +585,32 @@ impl ManagedAccountCollection { AccountType::CoinJoin { index, } => { - // CoinJoin addresses live on the external branch (m/9'/coin'/4'/account'/0/index) - // to match Dash Core, so derive through the `External` pool which uses [0, index]. - let mut coinjoin_path = base_path; - coinjoin_path.push(crate::bip32::ChildNumber::from_normal_idx(0)?); - let addresses = AddressPool::new( - coinjoin_path, + // Dual-pool: external (.../0/index) for Dash Core mixed coins, internal + // (.../1/index) for DashSync mixing-change. Watch both so no funds are missed. + let mut external_path = base_path.clone(); + external_path.push(crate::bip32::ChildNumber::from_normal_idx(0)?); + let external_addresses = AddressPool::new( + external_path, AddressPoolType::External, DEFAULT_COINJOIN_GAP_LIMIT, network, key_source, )?; + + let mut internal_path = base_path; + internal_path.push(crate::bip32::ChildNumber::from_normal_idx(1)?); + let internal_addresses = AddressPool::new( + internal_path, + AddressPoolType::Internal, + DEFAULT_COINJOIN_GAP_LIMIT, + network, + key_source, + )?; + ManagedAccountType::CoinJoin { index, - addresses, + external_addresses, + internal_addresses, } } AccountType::IdentityRegistration => { diff --git a/key-wallet/src/managed_account/managed_account_trait.rs b/key-wallet/src/managed_account/managed_account_trait.rs index dfbb46b28..a8817c9ae 100644 --- a/key-wallet/src/managed_account/managed_account_trait.rs +++ b/key-wallet/src/managed_account/managed_account_trait.rs @@ -133,7 +133,7 @@ pub trait ManagedAccountTrait { .. } => self.get_next_receive_address_index(), ManagedAccountType::CoinJoin { - addresses, + external_addresses: addresses, .. } | ManagedAccountType::IdentityRegistration { @@ -237,7 +237,7 @@ pub trait ManagedAccountTrait { .. } => Err("Standard accounts must use next_receive_address or next_change_address"), ManagedAccountType::CoinJoin { - addresses, + external_addresses: addresses, .. } | ManagedAccountType::IdentityRegistration { @@ -320,7 +320,7 @@ pub trait ManagedAccountTrait { "Standard accounts must use next_receive_address_with_info or next_change_address_with_info", ), ManagedAccountType::CoinJoin { - addresses, + external_addresses: addresses, .. } | ManagedAccountType::IdentityRegistration { @@ -559,7 +559,7 @@ pub trait ManagedAccountTrait { .. } => None, ManagedAccountType::CoinJoin { - addresses, + external_addresses: addresses, .. } | ManagedAccountType::IdentityRegistration { diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index d0f0492a4..40295e800 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -29,12 +29,18 @@ pub enum ManagedAccountType { /// Internal (change) address pool internal_addresses: AddressPool, }, - /// CoinJoin account for private transactions + /// CoinJoin account for private transactions. + /// + /// Dual-pool like `Standard`: Dash Core receives mixed coins on the external branch + /// (m/9'/coin'/4'/account'/0/index), while DashSync also uses the internal branch + /// (.../1/index) for mixing-change, so both chains must be watched to see all funds. CoinJoin { /// Account index index: u32, - /// CoinJoin address pool - addresses: AddressPool, + /// External (mixed-coin receive) address pool — .../0/index + external_addresses: AddressPool, + /// Internal (mixing-change) address pool — .../1/index + internal_addresses: AddressPool, }, /// Identity registration funding IdentityRegistration { @@ -211,14 +217,15 @@ impl ManagedAccountType { external_addresses, internal_addresses, .. - } => { - vec![external_addresses, internal_addresses] } - Self::CoinJoin { - addresses, + | Self::CoinJoin { + external_addresses, + internal_addresses, .. + } => { + vec![external_addresses, internal_addresses] } - | Self::IdentityRegistration { + Self::IdentityRegistration { addresses, .. } @@ -282,14 +289,15 @@ impl ManagedAccountType { external_addresses, internal_addresses, .. - } => { - vec![external_addresses, internal_addresses] } - Self::CoinJoin { - addresses, + | Self::CoinJoin { + external_addresses, + internal_addresses, .. + } => { + vec![external_addresses, internal_addresses] } - | Self::IdentityRegistration { + Self::IdentityRegistration { addresses, .. } @@ -529,23 +537,37 @@ impl ManagedAccountType { AccountType::CoinJoin { index, } => { - // CoinJoin addresses live on the external branch (m/9'/coin'/4'/account'/0/index) - // to match Dash Core, so derive through the `External` pool which uses [0, index]. - let mut path = account_type + // Dual-pool: Dash Core receives mixed coins on the external branch + // (m/9'/coin'/4'/account'/0/index); DashSync also uses the internal branch + // (.../1/index) for mixing-change. Watch both so no funds are missed. + let base_path = account_type .derivation_path(network) .unwrap_or_else(|_| DerivationPath::master()); - path.push(crate::bip32::ChildNumber::from_normal_idx(0)?); - let pool = AddressPool::new( - path, + + let mut external_path = base_path.clone(); + external_path.push(crate::bip32::ChildNumber::from_normal_idx(0)?); + let external_pool = AddressPool::new( + external_path, AddressPoolType::External, DEFAULT_COINJOIN_GAP_LIMIT, network, key_source, )?; + let mut internal_path = base_path; + internal_path.push(crate::bip32::ChildNumber::from_normal_idx(1)?); + let internal_pool = AddressPool::new( + internal_path, + AddressPoolType::Internal, + DEFAULT_COINJOIN_GAP_LIMIT, + network, + key_source, + )?; + Ok(Self::CoinJoin { index, - addresses: pool, + external_addresses: external_pool, + internal_addresses: internal_pool, }) } AccountType::IdentityRegistration => { diff --git a/key-wallet/src/tests/account_tests.rs b/key-wallet/src/tests/account_tests.rs index 1732823db..f788c46ec 100644 --- a/key-wallet/src/tests/account_tests.rs +++ b/key-wallet/src/tests/account_tests.rs @@ -142,9 +142,9 @@ fn test_coinjoin_account_creation() { .unwrap(); let pool = match &managed_type { ManagedAccountType::CoinJoin { - addresses, + external_addresses, .. - } => addresses, + } => external_addresses, _ => panic!("Expected CoinJoin managed account type"), }; let first_address = diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index 3ff50840c..bbff4ba8f 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -556,6 +556,11 @@ impl ManagedCoreFundsAccount { external_addresses, internal_addresses, .. + } + | ManagedAccountType::CoinJoin { + external_addresses, + internal_addresses, + .. } => { if external_addresses.contains_address(address) { AddressClassification::External @@ -565,16 +570,6 @@ impl ManagedCoreFundsAccount { AddressClassification::Other } } - ManagedAccountType::CoinJoin { - addresses, - .. - } => { - if addresses.contains_address(address) { - AddressClassification::External - } else { - AddressClassification::Other - } - } _ => { // For non-standard accounts, all addresses are "Other" AddressClassification::Other @@ -725,10 +720,9 @@ impl ManagedCoreFundsAccount { .. } => CoreAccountTypeMatch::CoinJoin { account_index: index.unwrap_or(0), - // For CoinJoin, use both receive addresses and other addresses - // since CoinJoin addresses can be classified as either involved_addresses: { let mut all_addresses = involved_receive_addresses.clone(); + all_addresses.extend(involved_change_addresses.clone()); all_addresses.extend(involved_other_addresses.clone()); all_addresses }, diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/conversions.rs b/key-wallet/src/transaction_checking/transaction_router/tests/conversions.rs index 1620447ca..309805bcc 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/conversions.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/conversions.rs @@ -52,7 +52,8 @@ fn test_try_from_standard_bip32_account() { fn test_try_from_coinjoin_account() { let managed_account = ManagedAccountType::CoinJoin { index: 0, - addresses: test_single_pool(), + external_addresses: test_single_pool(), + internal_addresses: test_single_pool(), }; let check_type: AccountTypeToCheck = managed_account.try_into().unwrap(); @@ -181,7 +182,8 @@ fn test_try_from_ref_all_account_types() { ( ManagedAccountType::CoinJoin { index: 0, - addresses: test_single_pool(), + external_addresses: test_single_pool(), + internal_addresses: test_single_pool(), }, AccountTypeToCheck::CoinJoin, ), diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs index b5a49a1d5..76568c45f 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs @@ -180,13 +180,13 @@ async fn test_transaction_routing_to_coinjoin_account() { .first_coinjoin_managed_account_mut() .expect("Failed to get first CoinJoin managed account"); let ManagedAccountType::CoinJoin { - addresses, + external_addresses, .. } = managed_account.managed_account_type_mut() else { panic!("Expected CoinJoin account type"); }; - addresses + external_addresses .next_unused(&KeySource::Public(xpub), true) .expect("Failed to derive CoinJoin address") }; diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 9a5f803bf..b62def243 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -1645,13 +1645,15 @@ mod tests { let managed_account = managed_wallet.first_coinjoin_managed_account_mut().expect("managed coinjoin"); - // Get an address from the CoinJoin pool + // Get an address from the CoinJoin external (receive) pool let coinjoin_address = if let ManagedAccountType::CoinJoin { - addresses, + external_addresses, .. } = managed_account.managed_account_type_mut() { - addresses.next_unused(&KeySource::Public(xpub), true).expect("coinjoin address") + external_addresses + .next_unused(&KeySource::Public(xpub), true) + .expect("coinjoin address") } else { panic!("Expected CoinJoin account type"); };