diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 0e5dba7ef..83b4ce810 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -92,6 +92,10 @@ typedef enum { PROVIDER_PLATFORM_KEYS = 10, DASHPAY_RECEIVING_FUNDS = 11, DASHPAY_EXTERNAL_ACCOUNT = 12, + /* + Platform Payment address (DIP-17) - Path: m/9'/5'/17'/account'/key_class'/index + */ + PLATFORM_PAYMENT = 13, } FFIAccountType; /* diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index c1f99a23f..506c1b4b4 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -59,6 +59,12 @@ fn get_managed_account_by_type<'a>( // DashPay managed accounts are not currently persisted in ManagedAccountCollection None } + AccountType::PlatformPayment { + .. + } => { + // Platform Payment accounts are not currently persisted in ManagedAccountCollection + None + } } } @@ -102,6 +108,12 @@ fn get_managed_account_by_type_mut<'a>( // DashPay managed accounts are not currently persisted in ManagedAccountCollection None } + AccountType::PlatformPayment { + .. + } => { + // Platform Payment accounts are not currently persisted in ManagedAccountCollection + None + } } } diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index 0be10f68f..5fb71c263 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -178,6 +178,9 @@ pub unsafe extern "C" fn managed_wallet_get_account( AccountType::DashpayExternalAccount { .. } => None, + AccountType::PlatformPayment { + .. + } => None, }; match managed_account { @@ -527,6 +530,9 @@ pub unsafe extern "C" fn managed_account_get_account_type( AccountType::DashpayExternalAccount { .. } => FFIAccountType::DashpayExternalAccount, + AccountType::PlatformPayment { + .. + } => FFIAccountType::PlatformPayment, } } @@ -1010,6 +1016,10 @@ pub unsafe extern "C" fn managed_account_get_address_pool( addresses, .. } => addresses, + ManagedAccountType::PlatformPayment { + addresses, + .. + } => addresses, }; let ffi_pool = FFIAddressPool { diff --git a/key-wallet-ffi/src/transaction_checking.rs b/key-wallet-ffi/src/transaction_checking.rs index 6c05093e7..fdadd8600 100644 --- a/key-wallet-ffi/src/transaction_checking.rs +++ b/key-wallet-ffi/src/transaction_checking.rs @@ -455,6 +455,27 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( ffi_accounts.push(ffi_match); continue; } + AccountTypeMatch::PlatformPayment { + account_index, + involved_addresses, + .. + } => { + // Note: Platform Payment addresses are NOT used in Core chain transactions + // per DIP-17. This branch should never be reached in practice. + let ffi_match = FFIAccountMatch { + account_type: 13, // PlatformPayment + account_index: *account_index, + registration_index: 0, + received: account_match.received, + sent: account_match.sent, + external_addresses_count: involved_addresses.len() as c_uint, + internal_addresses_count: 0, + has_external_addresses: !involved_addresses.is_empty(), + has_internal_addresses: false, + }; + ffi_accounts.push(ffi_match); + continue; + } } } diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index 3dcb0f48e..52715c69c 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -266,6 +266,8 @@ pub enum FFIAccountType { ProviderPlatformKeys = 10, DashpayReceivingFunds = 11, DashpayExternalAccount = 12, + /// Platform Payment address (DIP-17) - Path: m/9'/5'/17'/account'/key_class'/index + PlatformPayment = 13, } impl FFIAccountType { @@ -327,6 +329,14 @@ impl FFIAccountType { DashPay account creation must use a different API path." ); } + FFIAccountType::PlatformPayment => { + panic!( + "FFIAccountType::PlatformPayment cannot be converted to AccountType \ + without account and key_class indices. The FFI API does not yet \ + support passing these values. This is a programming error - \ + Platform Payment account creation must use a different API path." + ); + } } } @@ -411,6 +421,10 @@ impl FFIAccountType { &friend_identity_id[..8] ); } + key_wallet::AccountType::PlatformPayment { + account, + key_class, + } => (FFIAccountType::PlatformPayment, *account, Some(*key_class)), } } } @@ -433,6 +447,13 @@ mod tests { let _ = FFIAccountType::DashpayExternalAccount.to_account_type(0); } + #[test] + #[should_panic(expected = "PlatformPayment cannot be converted to AccountType")] + fn test_platform_payment_to_account_type_panics() { + // This should panic because we cannot construct a Platform Payment account without indices + let _ = FFIAccountType::PlatformPayment.to_account_type(0); + } + #[test] #[should_panic(expected = "Cannot convert AccountType::DashpayReceivingFunds")] fn test_dashpay_receiving_funds_from_account_type_panics() { diff --git a/key-wallet/src/account/account_collection.rs b/key-wallet/src/account/account_collection.rs index b84aea6cf..eb9dbaaad 100644 --- a/key-wallet/src/account/account_collection.rs +++ b/key-wallet/src/account/account_collection.rs @@ -28,6 +28,17 @@ pub struct DashpayAccountKey { pub friend_identity_id: DashpayContactIdentityId, } +/// Key for Platform Payment accounts (DIP-17) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +pub struct PlatformPaymentAccountKey { + /// Account index (hardened) + pub account: u32, + /// Key class (hardened) + pub key_class: u32, +} + /// Collection of accounts organized by type #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -61,6 +72,8 @@ pub struct AccountCollection { pub dashpay_receival_accounts: BTreeMap, /// DashPay external (watch-only) accounts pub dashpay_external_accounts: BTreeMap, + /// Platform Payment accounts (DIP-17) + pub platform_payment_accounts: BTreeMap, } impl AccountCollection { @@ -82,6 +95,7 @@ impl AccountCollection { provider_platform_keys: None, dashpay_receival_accounts: BTreeMap::new(), dashpay_external_accounts: BTreeMap::new(), + platform_payment_accounts: BTreeMap::new(), } } @@ -157,6 +171,16 @@ impl AccountCollection { }; self.dashpay_external_accounts.insert(key, account); } + AccountType::PlatformPayment { + account: acc_index, + key_class, + } => { + let key = PlatformPaymentAccountKey { + account: *acc_index, + key_class: *key_class, + }; + self.platform_payment_accounts.insert(key, account); + } } Ok(()) } @@ -240,6 +264,16 @@ impl AccountCollection { }; self.dashpay_external_accounts.contains_key(&key) } + AccountType::PlatformPayment { + account, + key_class, + } => { + let key = PlatformPaymentAccountKey { + account: *account, + key_class: *key_class, + }; + self.platform_payment_accounts.contains_key(&key) + } } } @@ -293,6 +327,16 @@ impl AccountCollection { }; self.dashpay_external_accounts.get(&key) } + AccountType::PlatformPayment { + account, + key_class, + } => { + let key = PlatformPaymentAccountKey { + account, + key_class, + }; + self.platform_payment_accounts.get(&key) + } } } @@ -346,6 +390,16 @@ impl AccountCollection { }; self.dashpay_external_accounts.get_mut(&key) } + AccountType::PlatformPayment { + account, + key_class, + } => { + let key = PlatformPaymentAccountKey { + account, + key_class, + }; + self.platform_payment_accounts.get_mut(&key) + } } } @@ -382,6 +436,10 @@ impl AccountCollection { // Note: provider_operator_keys (BLS) and provider_platform_keys (EdDSA) are excluded // Use specific methods to access them + accounts.extend(self.dashpay_receival_accounts.values()); + accounts.extend(self.dashpay_external_accounts.values()); + accounts.extend(self.platform_payment_accounts.values()); + accounts } @@ -418,6 +476,10 @@ impl AccountCollection { // Note: provider_operator_keys (BLS) and provider_platform_keys (EdDSA) are excluded // Use specific methods to access them + accounts.extend(self.dashpay_receival_accounts.values_mut()); + accounts.extend(self.dashpay_external_accounts.values_mut()); + accounts.extend(self.platform_payment_accounts.values_mut()); + accounts } diff --git a/key-wallet/src/account/account_type.rs b/key-wallet/src/account/account_type.rs index b1b0c7de5..3286ddb1c 100644 --- a/key-wallet/src/account/account_type.rs +++ b/key-wallet/src/account/account_type.rs @@ -83,6 +83,15 @@ pub enum AccountType { /// Our contact's identity id (32 bytes) friend_identity_id: [u8; 32], }, + /// Platform Payment account (DIP-17) + /// Path: m/9'/coin_type'/17'/account'/key_class'/index + /// Address encoding (DIP-18 bech32m) is handled by the Platform repo. + PlatformPayment { + /// Account index (hardened) - default 0' + account: u32, + /// Key class (hardened) - default 0', 1' reserved for change-like segregation + key_class: u32, + }, } impl From for AccountTypeToCheck { @@ -116,6 +125,9 @@ impl From for AccountTypeToCheck { AccountType::DashpayExternalAccount { .. } => AccountTypeToCheck::DashpayExternalAccount, + AccountType::PlatformPayment { + .. + } => AccountTypeToCheck::PlatformPayment, } } } @@ -140,6 +152,10 @@ impl AccountType { index, .. } => Some(*index), + Self::PlatformPayment { + account, + .. + } => Some(*account), // Identity and provider types don't have account indices Self::IdentityRegistration | Self::IdentityTopUp { @@ -208,6 +224,9 @@ impl AccountType { Self::DashpayExternalAccount { .. } => DerivationPathReference::ContactBasedFundsExternal, + Self::PlatformPayment { + .. + } => DerivationPathReference::PlatformPayment, } } @@ -392,6 +411,30 @@ impl AccountType { }); Ok(path) } + Self::PlatformPayment { + account, + key_class, + } => { + // DIP-17: m/9'/coin_type'/17'/account'/key_class' + // The leaf index is non-hardened and appended during address generation + let mut path = match network { + Network::Dash => { + DerivationPath::from(crate::dip9::PLATFORM_PAYMENT_ROOT_PATH_MAINNET) + } + Network::Testnet => { + DerivationPath::from(crate::dip9::PLATFORM_PAYMENT_ROOT_PATH_TESTNET) + } + _ => return Err(crate::error::Error::InvalidNetwork), + }; + path.push( + ChildNumber::from_hardened_idx(*account).map_err(crate::error::Error::Bip32)?, + ); + path.push( + ChildNumber::from_hardened_idx(*key_class) + .map_err(crate::error::Error::Bip32)?, + ); + Ok(path) + } } } } diff --git a/key-wallet/src/dip9.rs b/key-wallet/src/dip9.rs index d1b279359..76f6ca0e8 100644 --- a/key-wallet/src/dip9.rs +++ b/key-wallet/src/dip9.rs @@ -27,6 +27,7 @@ pub enum DerivationPathReference { BlockchainIdentityCreditInvitationFunding = 13, ProviderPlatformNodeKeys = 14, CoinJoin = 15, + PlatformPayment = 16, Root = 255, } @@ -131,6 +132,8 @@ pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_REGISTRATION: u32 = 1; pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_TOPUP: u32 = 2; pub const FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_INVITATIONS: u32 = 3; pub const FEATURE_PURPOSE_DASHPAY: u32 = 15; +/// DIP-17: Platform Payment Addresses feature index +pub const FEATURE_PURPOSE_PLATFORM_PAYMENT: u32 = 17; pub const DASH_BIP44_PATH_MAINNET: IndexConstPath<2> = IndexConstPath { indexes: [ ChildNumber::Hardened { @@ -376,3 +379,41 @@ pub const IDENTITY_AUTHENTICATION_PATH_TESTNET: IndexConstPath<4> = IndexConstPa reference: DerivationPathReference::BlockchainIdentities, path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION, }; + +// DIP-17: Platform Payment Address Paths +// Path: m/9'/coin_type'/17'/account'/key_class'/index +// Note: The full path includes account'/key_class'/index which is appended during derivation + +/// Platform Payment root path for mainnet: m/9'/5'/17' +pub const PLATFORM_PAYMENT_ROOT_PATH_MAINNET: IndexConstPath<3> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_PLATFORM_PAYMENT, + }, + ], + reference: DerivationPathReference::PlatformPayment, + path_type: DerivationPathType::CLEAR_FUNDS, +}; + +/// Platform Payment root path for testnet: m/9'/1'/17' +pub const PLATFORM_PAYMENT_ROOT_PATH_TESTNET: IndexConstPath<3> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_TESTNET_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_PLATFORM_PAYMENT, + }, + ], + reference: DerivationPathReference::PlatformPayment, + path_type: DerivationPathType::CLEAR_FUNDS, +}; diff --git a/key-wallet/src/gap_limit.rs b/key-wallet/src/gap_limit.rs index 688d8af4a..9d4d5b088 100644 --- a/key-wallet/src/gap_limit.rs +++ b/key-wallet/src/gap_limit.rs @@ -21,6 +21,10 @@ pub const DEFAULT_COINJOIN_GAP_LIMIT: u32 = 30; /// Standard gap limit for special purpose keys (identity, provider keys) pub const DEFAULT_SPECIAL_GAP_LIMIT: u32 = 5; + +/// Gap limit for DIP-17 platform payment addresses +pub const DIP17_GAP_LIMIT: u32 = 20; + /// Maximum gap limit to prevent excessive address generation pub const MAX_GAP_LIMIT: u32 = 1000; diff --git a/key-wallet/src/managed_account/address_pool.rs b/key-wallet/src/managed_account/address_pool.rs index cfef783f9..50100d3be 100644 --- a/key-wallet/src/managed_account/address_pool.rs +++ b/key-wallet/src/managed_account/address_pool.rs @@ -475,18 +475,9 @@ impl AddressPool { // Standard ECDSA address generation let dash_pubkey = dashcore::PublicKey::new(xpub.public_key); let network = self.network; - let address = match self.address_type { - AddressType::P2pkh => Address::p2pkh(&dash_pubkey, network), - AddressType::P2sh => { - // For P2SH, we'd need script information - // For now, default to P2PKH - Address::p2pkh(&dash_pubkey, network) - } - _ => { - // For other address types, default to P2PKH - Address::p2pkh(&dash_pubkey, network) - } - }; + // Generate P2PKH address (Platform addresses use the same underlying + // hash but with different encoding handled by the Platform repo) + let address = Address::p2pkh(&dash_pubkey, network); let public_key_bytes = dash_pubkey.to_bytes(); (address, PublicKeyType::ECDSA(public_key_bytes.to_vec())) } diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 85e1614a4..5ada3f50a 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -3,11 +3,11 @@ //! This module provides a structure for managing multiple accounts //! across different networks in a hierarchical manner. -use crate::account::account_collection::DashpayAccountKey; +use crate::account::account_collection::{DashpayAccountKey, PlatformPaymentAccountKey}; use crate::account::account_type::AccountType; use crate::gap_limit::{ DEFAULT_COINJOIN_GAP_LIMIT, DEFAULT_EXTERNAL_GAP_LIMIT, DEFAULT_INTERNAL_GAP_LIMIT, - DEFAULT_SPECIAL_GAP_LIMIT, + DEFAULT_SPECIAL_GAP_LIMIT, DIP17_GAP_LIMIT, }; use crate::managed_account::address_pool::{AddressPool, AddressPoolType}; use crate::managed_account::managed_account_type::ManagedAccountType; @@ -49,6 +49,8 @@ pub struct ManagedAccountCollection { pub dashpay_receival_accounts: BTreeMap, /// DashPay external accounts keyed by (index, user_id, friend_id) pub dashpay_external_accounts: BTreeMap, + /// Platform Payment accounts (DIP-17) + pub platform_payment_accounts: BTreeMap, } impl ManagedAccountCollection { @@ -68,6 +70,7 @@ impl ManagedAccountCollection { provider_platform_keys: None, dashpay_receival_accounts: BTreeMap::new(), dashpay_external_accounts: BTreeMap::new(), + platform_payment_accounts: BTreeMap::new(), } } @@ -143,6 +146,17 @@ impl ManagedAccountCollection { }; self.dashpay_external_accounts.contains_key(&key) } + ManagedAccountType::PlatformPayment { + account, + key_class, + .. + } => { + let key = PlatformPaymentAccountKey { + account: *account, + key_class: *key_class, + }; + self.platform_payment_accounts.contains_key(&key) + } } } @@ -236,6 +250,17 @@ impl ManagedAccountCollection { }; self.dashpay_external_accounts.insert(key, account); } + ManagedAccountType::PlatformPayment { + account: acc_index, + key_class, + .. + } => { + let key = PlatformPaymentAccountKey { + account: *acc_index, + key_class: *key_class, + }; + self.platform_payment_accounts.insert(key, account); + } } } @@ -332,6 +357,13 @@ impl ManagedAccountCollection { } } + // Convert Platform Payment accounts + for (key, account) in &account_collection.platform_payment_accounts { + if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + managed_collection.platform_payment_accounts.insert(*key, managed_account); + } + } + managed_collection } @@ -572,6 +604,24 @@ impl ManagedAccountCollection { addresses, } } + AccountType::PlatformPayment { + account, + key_class, + } => { + // DIP-17/DIP-18: Platform Payment addresses + let addresses = AddressPool::new( + base_path, + AddressPoolType::Absent, + DIP17_GAP_LIMIT, + network, + key_source, + )?; + ManagedAccountType::PlatformPayment { + account, + key_class, + addresses, + } + } }; Ok(ManagedAccount::new(managed_type, network, is_watch_only)) diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index 8dbc2e805..a45e8e766 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -2,7 +2,7 @@ use crate::account::account_collection::{DashpayContactIdentityId, DashpayOurUse use crate::account::StandardAccountType; use crate::gap_limit::{ DEFAULT_COINJOIN_GAP_LIMIT, DEFAULT_EXTERNAL_GAP_LIMIT, DEFAULT_INTERNAL_GAP_LIMIT, - DEFAULT_SPECIAL_GAP_LIMIT, + DEFAULT_SPECIAL_GAP_LIMIT, DIP17_GAP_LIMIT, }; use crate::{AccountType, AddressPool, DerivationPath}; @@ -104,6 +104,17 @@ pub enum ManagedAccountType { /// Address pool addresses: AddressPool, }, + /// Platform Payment account (DIP-17) + /// Path: m/9'/coin_type'/17'/account'/key_class'/index + /// Address encoding (DIP-18 bech32m) is handled by the Platform repo. + PlatformPayment { + /// Account index (hardened) + account: u32, + /// Key class (hardened) + key_class: u32, + /// Platform payment address pool (single pool, non-hardened leaf index) + addresses: AddressPool, + }, } impl ManagedAccountType { @@ -152,6 +163,10 @@ impl ManagedAccountType { index, .. } => Some(*index), + Self::PlatformPayment { + account, + .. + } => Some(*account), } } @@ -226,6 +241,10 @@ impl ManagedAccountType { | Self::DashpayExternalAccount { addresses, .. + } + | Self::PlatformPayment { + addresses, + .. } => vec![addresses], } } @@ -285,6 +304,10 @@ impl ManagedAccountType { | Self::DashpayExternalAccount { addresses, .. + } + | Self::PlatformPayment { + addresses, + .. } => vec![addresses], } } @@ -401,6 +424,14 @@ impl ManagedAccountType { user_identity_id: *user_identity_id, friend_identity_id: *friend_identity_id, }, + Self::PlatformPayment { + account, + key_class, + .. + } => AccountType::PlatformPayment { + account: *account, + key_class: *key_class, + }, } } @@ -644,6 +675,28 @@ impl ManagedAccountType { addresses: pool, }) } + AccountType::PlatformPayment { + account, + key_class, + } => { + // DIP-17: m/9'/coin_type'/17'/account'/key_class'/index + // The leaf index is non-hardened + let path = account_type + .derivation_path(network) + .unwrap_or_else(|_| DerivationPath::master()); + let pool = AddressPool::new( + path, + crate::managed_account::address_pool::AddressPoolType::Absent, + DIP17_GAP_LIMIT, + network, + key_source, + )?; + Ok(Self::PlatformPayment { + account, + key_class, + addresses: pool, + }) + } } } } diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index a55248a36..94fea9d6f 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -244,6 +244,10 @@ impl ManagedAccount { | ManagedAccountType::DashpayExternalAccount { addresses, .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. } => { addresses.unused_addresses().first().and_then(|addr| addresses.address_index(addr)) } @@ -492,6 +496,10 @@ impl ManagedAccount { | ManagedAccountType::DashpayExternalAccount { addresses, .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -577,6 +585,10 @@ impl ManagedAccount { | ManagedAccountType::DashpayExternalAccount { addresses, .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -802,6 +814,10 @@ impl ManagedAccount { | ManagedAccountType::DashpayExternalAccount { addresses, .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. } => Some(addresses.gap_limit), } } diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index 9048bdf0b..3ea246d95 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -103,6 +103,13 @@ pub enum AccountTypeMatch { account_index: u32, involved_addresses: Vec, }, + /// Platform Payment account (DIP-17) + /// Note: Platform addresses are NOT used in Core chain transactions + PlatformPayment { + account_index: u32, + key_class: u32, + involved_addresses: Vec, + }, } impl AccountTypeMatch { @@ -159,6 +166,10 @@ impl AccountTypeMatch { | AccountTypeMatch::DashpayExternalAccount { involved_addresses, .. + } + | AccountTypeMatch::PlatformPayment { + involved_addresses, + .. } => involved_addresses.clone(), } } @@ -189,6 +200,10 @@ impl AccountTypeMatch { | AccountTypeMatch::DashpayExternalAccount { account_index, .. + } + | AccountTypeMatch::PlatformPayment { + account_index, + .. } => Some(*account_index), _ => None, } @@ -236,6 +251,9 @@ impl AccountTypeMatch { AccountTypeMatch::DashpayExternalAccount { .. } => AccountTypeToCheck::DashpayExternalAccount, + AccountTypeMatch::PlatformPayment { + .. + } => AccountTypeToCheck::PlatformPayment, } } } @@ -366,6 +384,12 @@ impl ManagedAccountCollection { } matches } + AccountTypeToCheck::PlatformPayment => { + // Platform Payment addresses (DIP-17) are NOT used in Core chain transactions. + // They are only for Platform-side payments. This account type should never match + // any Core chain transaction by design. + Vec::new() + } } } @@ -621,6 +645,15 @@ impl ManagedAccount { account_index: index.unwrap_or(0), involved_addresses: involved_other_addresses, }, + ManagedAccountType::PlatformPayment { + account, + key_class, + .. + } => AccountTypeMatch::PlatformPayment { + account_index: *account, + key_class: *key_class, + involved_addresses: involved_other_addresses, + }, }; Some(AccountMatch { diff --git a/key-wallet/src/transaction_checking/transaction_router/mod.rs b/key-wallet/src/transaction_checking/transaction_router/mod.rs index 19e9bd06c..36fde7583 100644 --- a/key-wallet/src/transaction_checking/transaction_router/mod.rs +++ b/key-wallet/src/transaction_checking/transaction_router/mod.rs @@ -178,6 +178,9 @@ pub enum AccountTypeToCheck { ProviderPlatformKeys, DashpayReceivingFunds, DashpayExternalAccount, + /// Platform Payment accounts (DIP-17). + /// Note: These are NOT checked for Core chain transactions as they operate on Dash Platform. + PlatformPayment, } impl From for AccountTypeToCheck { @@ -227,6 +230,9 @@ impl From for AccountTypeToCheck { ManagedAccountType::DashpayExternalAccount { .. } => AccountTypeToCheck::DashpayExternalAccount, + ManagedAccountType::PlatformPayment { + .. + } => AccountTypeToCheck::PlatformPayment, } } } @@ -278,6 +284,9 @@ impl From<&ManagedAccountType> for AccountTypeToCheck { ManagedAccountType::DashpayExternalAccount { .. } => AccountTypeToCheck::DashpayExternalAccount, + ManagedAccountType::PlatformPayment { + .. + } => AccountTypeToCheck::PlatformPayment, } } } diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 722f45deb..be9c35165 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -141,6 +141,13 @@ impl WalletTransactionChecker for ManagedWalletInfo { // DashPay managed accounts are not persisted here yet None } + AccountTypeMatch::PlatformPayment { + .. + } => { + // Platform Payment addresses are NOT used in Core chain transactions. + // This branch should never be reached by design (per DIP-17). + None + } }; if let Some(account) = account { diff --git a/key-wallet/src/wallet/helper.rs b/key-wallet/src/wallet/helper.rs index a9ac07be3..8e4167d94 100644 --- a/key-wallet/src/wallet/helper.rs +++ b/key-wallet/src/wallet/helper.rs @@ -873,6 +873,11 @@ impl Wallet { // Currently not retrieved via this helper None } + crate::transaction_checking::transaction_router::AccountTypeToCheck::PlatformPayment => { + // Platform Payment addresses are not used in Core chain transactions + // and xpubs are not retrieved via this helper + None + } } }) } diff --git a/key-wallet/tests/derivation_tests.rs b/key-wallet/tests/derivation_tests.rs index 6be6c7aa2..87478c921 100644 --- a/key-wallet/tests/derivation_tests.rs +++ b/key-wallet/tests/derivation_tests.rs @@ -1,5 +1,6 @@ //! Derivation tests +use dashcore::hashes::Hash; use key_wallet::derivation::{AccountDerivation, HDWallet}; use key_wallet::mnemonic::{Language, Mnemonic}; use key_wallet::{DerivationPath, ExtendedPubKey, Network}; @@ -109,3 +110,218 @@ fn test_public_key_derivation() { assert_eq!(xpub.public_key, xpub_from_prv.public_key); } + +// ============================================================================= +// DIP-17 Platform Payment Key Derivation Test Vectors +// ============================================================================= +// +// These tests verify the key derivation for Platform Payment addresses as +// specified in DIP-0017 (HD Derivation). The address encoding (DIP-18) uses +// bech32m with "dashevo"/"tdashevo" HRP and is implemented in the Platform repo. +// +// Test mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" +// Passphrase: "" (empty) + +/// DIP-17 Test Vector 1: Platform Payment key derivation (mainnet) +/// Path: m/9'/5'/17'/0'/0'/0 +/// Expected private key: 6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe +/// Expected pubkey: 03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e +/// Expected HASH160: f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 +#[test] +fn test_dip17_platform_payment_vector1_mainnet() { + use dashcore::crypto::key::PublicKey; + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English, + ) + .unwrap(); + + let seed = mnemonic.to_seed(""); + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Derive Platform Payment key: m/9'/5'/17'/0'/0'/0 + let path = DerivationPath::from_str("m/9'/5'/17'/0'/0'/0").unwrap(); + let xprv = wallet.derive(&path).unwrap(); + + // Verify private key matches DIP-17 test vector + let privkey_hex = hex::encode(xprv.private_key.secret_bytes()); + assert_eq!( + privkey_hex, "6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe", + "Private key mismatch for DIP-17 vector 1" + ); + + // Get compressed public key + let secp = Secp256k1::new(); + let xpub = ExtendedPubKey::from_priv(&secp, &xprv); + let pubkey = PublicKey::new(xpub.public_key); + let pubkey_hex = hex::encode(pubkey.to_bytes()); + assert_eq!( + pubkey_hex, "03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e", + "Public key mismatch for DIP-17 vector 1" + ); + + // Verify HASH160 + let pubkey_hash = pubkey.pubkey_hash(); + let hash160_hex = hex::encode(pubkey_hash.to_byte_array()); + assert_eq!( + hash160_hex, "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", + "HASH160 mismatch for DIP-17 vector 1" + ); + + // Note: DIP-18 address encoding (bech32m with "dashevo" HRP) is in Platform repo +} + +/// DIP-17 Test Vector 1: Platform Payment key derivation (testnet) +/// Path: m/9'/1'/17'/0'/0'/0 (note: coin_type 1' for testnet) +#[test] +fn test_dip17_platform_payment_vector1_testnet() { + use dashcore::crypto::key::PublicKey; + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English, + ) + .unwrap(); + + let seed = mnemonic.to_seed(""); + let wallet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); + + // Derive Platform Payment key: m/9'/1'/17'/0'/0'/0 (testnet uses coin_type 1') + let path = DerivationPath::from_str("m/9'/1'/17'/0'/0'/0").unwrap(); + let xprv = wallet.derive(&path).unwrap(); + + // Get compressed public key and HASH160 + let secp = Secp256k1::new(); + let xpub = ExtendedPubKey::from_priv(&secp, &xprv); + let pubkey = PublicKey::new(xpub.public_key); + let pubkey_hash = pubkey.pubkey_hash(); + + // Verify we can derive correctly (HASH160 will be used for bech32m encoding in Platform) + assert!(!pubkey_hash.to_byte_array().is_empty()); + + // Note: DIP-18 address encoding (bech32m with "tdashevo" HRP) is in Platform repo +} + +/// DIP-17 Test Vector 2: Platform Payment key derivation (index 1) +/// Path: m/9'/5'/17'/0'/0'/1 (mainnet) / m/9'/1'/17'/0'/0'/1 (testnet) +/// Expected private key: eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523 +/// Expected pubkey: 02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da +/// Expected HASH160: a5ff0046217fd1c7d238e3e146cc5bfd90832a7e +#[test] +fn test_dip17_platform_payment_vector2() { + use dashcore::crypto::key::PublicKey; + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English, + ) + .unwrap(); + + // Test mainnet + let seed = mnemonic.to_seed(""); + let wallet_mainnet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + let path_mainnet = DerivationPath::from_str("m/9'/5'/17'/0'/0'/1").unwrap(); + let xprv_mainnet = wallet_mainnet.derive(&path_mainnet).unwrap(); + + // Verify private key + let privkey_hex = hex::encode(xprv_mainnet.private_key.secret_bytes()); + assert_eq!( + privkey_hex, "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523", + "Private key mismatch for DIP-17 vector 2" + ); + + let secp = Secp256k1::new(); + let xpub_mainnet = ExtendedPubKey::from_priv(&secp, &xprv_mainnet); + let pubkey_mainnet = PublicKey::new(xpub_mainnet.public_key); + + // Verify public key + let pubkey_hex = hex::encode(pubkey_mainnet.to_bytes()); + assert_eq!( + pubkey_hex, "02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da", + "Public key mismatch for DIP-17 vector 2" + ); + + // Verify HASH160 + let pubkey_hash_mainnet = pubkey_mainnet.pubkey_hash(); + let hash160_hex = hex::encode(pubkey_hash_mainnet.to_byte_array()); + assert_eq!( + hash160_hex, "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", + "HASH160 mismatch for DIP-17 vector 2" + ); + + // Test testnet derivation + let wallet_testnet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); + let path_testnet = DerivationPath::from_str("m/9'/1'/17'/0'/0'/1").unwrap(); + let xprv_testnet = wallet_testnet.derive(&path_testnet).unwrap(); + let xpub_testnet = ExtendedPubKey::from_priv(&secp, &xprv_testnet); + let pubkey_testnet = PublicKey::new(xpub_testnet.public_key); + let pubkey_hash_testnet = pubkey_testnet.pubkey_hash(); + + // Verify testnet derivation produces valid hash + assert!(!pubkey_hash_testnet.to_byte_array().is_empty()); + + // Note: DIP-18 address encoding (bech32m) is in Platform repo +} + +/// DIP-17 Test Vector 3: Platform Payment key derivation with non-default key_class +/// Path: m/9'/5'/17'/0'/1'/0 (mainnet) / m/9'/1'/17'/0'/1'/0 (testnet) +/// Note: key_class' = 1' instead of default 0' +/// Expected private key: cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db +/// Expected pubkey: 0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3 +/// Expected HASH160: 6d92674fd64472a3dfcfc3ebcfed7382bf699d7b +#[test] +fn test_dip17_platform_payment_vector3_non_default_key_class() { + use dashcore::crypto::key::PublicKey; + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English, + ) + .unwrap(); + + // Test mainnet with key_class' = 1' + let seed = mnemonic.to_seed(""); + let wallet_mainnet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + let path_mainnet = DerivationPath::from_str("m/9'/5'/17'/0'/1'/0").unwrap(); + let xprv_mainnet = wallet_mainnet.derive(&path_mainnet).unwrap(); + + // Verify private key + let privkey_hex = hex::encode(xprv_mainnet.private_key.secret_bytes()); + assert_eq!( + privkey_hex, "cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db", + "Private key mismatch for DIP-17 vector 3" + ); + + let secp = Secp256k1::new(); + let xpub_mainnet = ExtendedPubKey::from_priv(&secp, &xprv_mainnet); + let pubkey_mainnet = PublicKey::new(xpub_mainnet.public_key); + + // Verify public key + let pubkey_hex = hex::encode(pubkey_mainnet.to_bytes()); + assert_eq!( + pubkey_hex, "0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3", + "Public key mismatch for DIP-17 vector 3" + ); + + // Verify HASH160 + let pubkey_hash_mainnet = pubkey_mainnet.pubkey_hash(); + let hash160_hex = hex::encode(pubkey_hash_mainnet.to_byte_array()); + assert_eq!( + hash160_hex, "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", + "HASH160 mismatch for DIP-17 vector 3" + ); + + // Test testnet with key_class' = 1' + let wallet_testnet = HDWallet::from_seed(&seed, Network::Testnet).unwrap(); + let path_testnet = DerivationPath::from_str("m/9'/1'/17'/0'/1'/0").unwrap(); + let xprv_testnet = wallet_testnet.derive(&path_testnet).unwrap(); + let xpub_testnet = ExtendedPubKey::from_priv(&secp, &xprv_testnet); + let pubkey_testnet = PublicKey::new(xpub_testnet.public_key); + let pubkey_hash_testnet = pubkey_testnet.pubkey_hash(); + + // Verify testnet derivation produces valid hash + assert!(!pubkey_hash_testnet.to_byte_array().is_empty()); + + // Note: DIP-18 address encoding (bech32m) is in Platform repo +}