diff --git a/key-wallet/src/account/account_type.rs b/key-wallet/src/account/account_type.rs index f5ce4961c..d027317bf 100644 --- a/key-wallet/src/account/account_type.rs +++ b/key-wallet/src/account/account_type.rs @@ -470,11 +470,13 @@ impl AccountType { ])) } Self::DashpayReceivingFunds { + index: account_index, user_identity_id, friend_identity_id, - .. } => { - // Base DashPay root + account 0' + user_id/friend_id (non-hardened per DIP-14/DIP-15) + // Base DashPay root + account' + user_id/friend_id (non-hardened per DIP-14/DIP-15). + // The account is the sender's DashPay account (DIP-15 "Account Reference"), + // hardened; defaults to 0 but may differ for multi-account wallets. let mut path = match network { Network::Mainnet => { DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_MAINNET) @@ -483,7 +485,10 @@ impl AccountType { DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_TESTNET) } }; - path.push(ChildNumber::from_hardened_idx(0).map_err(crate::error::Error::Bip32)?); + path.push( + ChildNumber::from_hardened_idx(*account_index) + .map_err(crate::error::Error::Bip32)?, + ); path.push(ChildNumber::Normal256 { index: *user_identity_id, }); @@ -493,11 +498,13 @@ impl AccountType { Ok(path) } Self::DashpayExternalAccount { + index: account_index, user_identity_id, friend_identity_id, - .. } => { - // Base DashPay root + account 0' + friend_id/user_id (non-hardened per DIP-14/DIP-15) + // Base DashPay root + account' + friend_id/user_id (non-hardened per DIP-14/DIP-15). + // The account is the sender's DashPay account (DIP-15 "Account Reference"), + // hardened; defaults to 0 but may differ for multi-account wallets. let mut path = match network { Network::Mainnet => { DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_MAINNET) @@ -506,7 +513,10 @@ impl AccountType { DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_TESTNET) } }; - path.push(ChildNumber::from_hardened_idx(0).map_err(crate::error::Error::Bip32)?); + path.push( + ChildNumber::from_hardened_idx(*account_index) + .map_err(crate::error::Error::Bip32)?, + ); path.push(ChildNumber::Normal256 { index: *friend_identity_id, }); diff --git a/key-wallet/src/tests/account_tests.rs b/key-wallet/src/tests/account_tests.rs index d21b238bf..1732823db 100644 --- a/key-wallet/src/tests/account_tests.rs +++ b/key-wallet/src/tests/account_tests.rs @@ -549,3 +549,82 @@ fn test_account_derivation_path_uniqueness() { paths.push(path_str); } } + +#[test] +fn test_dashpay_account_index_in_derivation_path() { + use crate::bip32::ChildNumber; + + // The DashPay friendship path is m/9'/coin'/15'/account'// (DIP-15 "Account + // Reference" + DIP-14 256-bit indices). The account segment is the sender's DashPay + // account and MUST reflect the account index carried on the account type — not a fixed + // 0'. A multi-account wallet that derived every relationship under account 0 would + // watch the wrong addresses and miss funds paid to a non-zero account. Account 0 must + // stay byte-identical to the legacy single-account path (backward compatibility). + let network = Network::Testnet; + let user_id = [0x11u8; 32]; + let friend_id = [0x22u8; 32]; + + for account in [0u32, 1, 7, 42] { + // Receiving funds: identity ids ordered user/friend. + let recv = AccountType::DashpayReceivingFunds { + index: account, + user_identity_id: user_id, + friend_identity_id: friend_id, + }; + let recv_path = recv.derivation_path(network).unwrap(); + let recv_comps: Vec = recv_path.clone().into(); + assert_eq!(recv_comps.len(), 6, "root(3) + account' + two 256-bit ids"); + assert_eq!( + recv_comps[3], + ChildNumber::Hardened { + index: account + }, + "receiving account segment must honor the account index" + ); + assert_eq!( + recv_comps[4], + ChildNumber::Normal256 { + index: user_id + } + ); + assert_eq!( + recv_comps[5], + ChildNumber::Normal256 { + index: friend_id + } + ); + assert!( + recv_path.to_string().starts_with(&format!("m/9'/1'/15'/{}'/", account)), + "receiving path should carry account {}': got {}", + account, + recv_path + ); + + // External account: the reverse channel, ids ordered friend/user. + let ext = AccountType::DashpayExternalAccount { + index: account, + user_identity_id: user_id, + friend_identity_id: friend_id, + }; + let ext_comps: Vec = ext.derivation_path(network).unwrap().into(); + assert_eq!( + ext_comps[3], + ChildNumber::Hardened { + index: account + }, + "external account segment must honor the account index" + ); + assert_eq!( + ext_comps[4], + ChildNumber::Normal256 { + index: friend_id + } + ); + assert_eq!( + ext_comps[5], + ChildNumber::Normal256 { + index: user_id + } + ); + } +}