diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index 2e0308c9a..71addafc9 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -3,7 +3,7 @@ #ifndef DASH_SPV_FFI_H #define DASH_SPV_FFI_H -/* Generated with cbindgen:0.29.0 */ +/* Generated with cbindgen:0.29.2 */ /* Warning: This file is auto-generated by cbindgen. Do not modify manually. */ @@ -16,11 +16,6 @@ namespace dash_spv_ffi { #endif // __cplusplus -typedef enum FFIMempoolStrategy { - FetchAll = 0, - BloomFilter = 1, -} FFIMempoolStrategy; - typedef enum FFISyncStage { Connecting = 0, QueryingHeight = 1, @@ -34,6 +29,11 @@ typedef enum FFISyncStage { Failed = 9, } FFISyncStage; +typedef enum FFIMempoolStrategy { + FetchAll = 0, + BloomFilter = 1, +} FFIMempoolStrategy; + typedef enum DashSpvValidationMode { None = 0, Basic = 1, diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index 0991ade09..c42372067 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 238 +**Total Functions**: 242 ## Table of Contents @@ -68,7 +68,7 @@ Functions: 19 ### Wallet Operations -Functions: 58 +Functions: 62 | Function | Description | Module | |----------|-------------|--------| @@ -90,6 +90,8 @@ Functions: 58 | `managed_wallet_get_balance` | Get wallet balance from managed wallet info Returns the balance breakdown in... | managed_wallet | | `managed_wallet_get_bip_44_external_address_range` | Get BIP44 external (receive) addresses in the specified range Returns extern... | managed_wallet | | `managed_wallet_get_bip_44_internal_address_range` | Get BIP44 internal (change) addresses in the specified range Returns interna... | managed_wallet | +| `managed_wallet_get_dashpay_external_account` | Get a managed DashPay external account by composite key # Safety - Pointers ... | managed_account | +| `managed_wallet_get_dashpay_receiving_account` | Get a managed DashPay receiving funds account by composite key # Safety - `m... | managed_account | | `managed_wallet_get_next_bip44_change_address` | Get the next unused change address Generates the next unused change address ... | managed_wallet | | `managed_wallet_get_next_bip44_receive_address` | Get the next unused receive address Generates the next unused receive addres... | managed_wallet | | `managed_wallet_get_top_up_account_with_registration_index` | Get a managed IdentityTopUp account with a specific registration index This ... | managed_account | @@ -100,6 +102,8 @@ Functions: 58 | `wallet_add_account` | Add an account to the wallet without xpub # Safety This function dereferenc... | wallet | | `wallet_add_account_with_string_xpub` | Add an account to the wallet with xpub as string # Safety This function der... | wallet | | `wallet_add_account_with_xpub_bytes` | Add an account to the wallet with xpub as byte array # Safety This function... | wallet | +| `wallet_add_dashpay_external_account_with_xpub_bytes` | Add a DashPay external (watch-only) account with xpub bytes # Safety - `wall... | wallet | +| `wallet_add_dashpay_receiving_account` | Add a DashPay receiving funds account # Safety - `wallet` must be a valid po... | wallet | | `wallet_build_and_sign_transaction` | Build and sign a transaction using the wallet's managed info This is the rec... | transaction | | `wallet_build_transaction` | Build a transaction (unsigned) This creates an unsigned transaction | transaction | | `wallet_check_transaction` | Check if a transaction belongs to the wallet using ManagedWalletInfo # Safet... | transaction | @@ -815,14 +819,14 @@ Get the parent wallet ID of a managed account Note: ManagedAccount doesn't stor #### `managed_wallet_check_transaction` ```c -managed_wallet_check_transaction(managed_wallet: *mut FFIManagedWalletInfo, wallet: *const FFIWallet, network: FFINetwork, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_height: c_uint, block_hash: *const u8, // 32 bytes if not null timestamp: u64, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool +managed_wallet_check_transaction(managed_wallet: *mut FFIManagedWalletInfo, wallet: *mut FFIWallet, network: FFINetwork, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_height: c_uint, block_hash: *const u8, // 32 bytes if not null timestamp: u64, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool ``` **Description:** -Check if a transaction belongs to the wallet This function checks a transaction against all relevant account types in the wallet and returns detailed information about which accounts are affected. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet (needed for address generation) - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError - The affected_accounts array in the result must be freed with `transaction_check_result_free` +Check if a transaction belongs to the wallet This function checks a transaction against all relevant account types in the wallet and returns detailed information about which accounts are affected. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet (needed for address generation and DashPay queries) - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError - The affected_accounts array in the result must be freed with `transaction_check_result_free` **Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet (needed for address generation) - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError - The affected_accounts array in the result must be freed with `transaction_check_result_free` +- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet (needed for address generation and DashPay queries) - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError - The affected_accounts array in the result must be freed with `transaction_check_result_free` **Module:** `transaction_checking` @@ -972,6 +976,38 @@ Get BIP44 internal (change) addresses in the specified range Returns internal a --- +#### `managed_wallet_get_dashpay_external_account` + +```c +managed_wallet_get_dashpay_external_account(manager: *const FFIWalletManager, wallet_id: *const u8, network: FFINetwork, account_index: c_uint, user_identity_id: *const u8, friend_identity_id: *const u8,) -> FFIManagedAccountResult +``` + +**Description:** +Get a managed DashPay external account by composite key # Safety - Pointers must be valid + +**Safety:** +- Pointers must be valid + +**Module:** `managed_account` + +--- + +#### `managed_wallet_get_dashpay_receiving_account` + +```c +managed_wallet_get_dashpay_receiving_account(manager: *const FFIWalletManager, wallet_id: *const u8, network: FFINetwork, account_index: c_uint, user_identity_id: *const u8, friend_identity_id: *const u8,) -> FFIManagedAccountResult +``` + +**Description:** +Get a managed DashPay receiving funds account by composite key # Safety - `manager`, `wallet_id` must be valid - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + +**Safety:** +- `manager`, `wallet_id` must be valid - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + +**Module:** `managed_account` + +--- + #### `managed_wallet_get_next_bip44_change_address` ```c @@ -1132,6 +1168,38 @@ This function dereferences raw pointers. The caller must ensure that: - The wall --- +#### `wallet_add_dashpay_external_account_with_xpub_bytes` + +```c +wallet_add_dashpay_external_account_with_xpub_bytes(wallet: *mut FFIWallet, network: FFINetwork, account_index: c_uint, user_identity_id: *const u8, friend_identity_id: *const u8, xpub_bytes: *const u8, xpub_len: usize,) -> FFIAccountResult +``` + +**Description:** +Add a DashPay external (watch-only) account with xpub bytes # Safety - `wallet` must be valid, `xpub_bytes` must point to `xpub_len` bytes - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + +**Safety:** +- `wallet` must be valid, `xpub_bytes` must point to `xpub_len` bytes - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + +**Module:** `wallet` + +--- + +#### `wallet_add_dashpay_receiving_account` + +```c +wallet_add_dashpay_receiving_account(wallet: *mut FFIWallet, network: FFINetwork, account_index: c_uint, user_identity_id: *const u8, friend_identity_id: *const u8,) -> FFIAccountResult +``` + +**Description:** +Add a DashPay receiving funds account # Safety - `wallet` must be a valid pointer - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + +**Safety:** +- `wallet` must be a valid pointer - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + +**Module:** `wallet` + +--- + #### `wallet_build_and_sign_transaction` ```c diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 2eea3c5fa..8dfba760e 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -106,6 +106,8 @@ typedef enum { Provider platform P2P keys (DIP-3, ED25519) - Path: m/9'/5'/3'/4'/[key_index] */ PROVIDER_PLATFORM_KEYS = 10, + DASHPAY_RECEIVING_FUNDS = 11, + DASHPAY_EXTERNAL_ACCOUNT = 12, } FFIAccountType; /* @@ -2418,6 +2420,37 @@ FFIManagedAccountResult managed_wallet_get_top_up_account_with_registration_inde unsigned int registration_index) ; +/* + Get a managed DashPay receiving funds account by composite key + + # Safety + - `manager`, `wallet_id` must be valid + - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + */ + +FFIManagedAccountResult managed_wallet_get_dashpay_receiving_account(const FFIWalletManager *manager, + const uint8_t *wallet_id, + FFINetwork network, + unsigned int account_index, + const uint8_t *user_identity_id, + const uint8_t *friend_identity_id) +; + +/* + Get a managed DashPay external account by composite key + + # Safety + - Pointers must be valid + */ + +FFIManagedAccountResult managed_wallet_get_dashpay_external_account(const FFIWalletManager *manager, + const uint8_t *wallet_id, + FFINetwork network, + unsigned int account_index, + const uint8_t *user_identity_id, + const uint8_t *friend_identity_id) +; + /* Get the network of a managed account @@ -3495,7 +3528,7 @@ FFIManagedWalletInfo *wallet_create_managed_wallet(const FFIWallet *wallet, # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - - `wallet` must be a valid pointer to an FFIWallet (needed for address generation) + - `wallet` must be a valid pointer to an FFIWallet (needed for address generation and DashPay queries) - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError @@ -3503,7 +3536,7 @@ FFIManagedWalletInfo *wallet_create_managed_wallet(const FFIWallet *wallet, */ bool managed_wallet_check_transaction(FFIManagedWalletInfo *managed_wallet, - const FFIWallet *wallet, + FFIWallet *wallet, FFINetwork network, const uint8_t *tx_bytes, size_t tx_len, @@ -3802,6 +3835,38 @@ FFIAccountResult wallet_add_account(FFIWallet *wallet, unsigned int account_index) ; +/* + Add a DashPay receiving funds account + + # Safety + - `wallet` must be a valid pointer + - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + */ + +FFIAccountResult wallet_add_dashpay_receiving_account(FFIWallet *wallet, + FFINetwork network, + unsigned int account_index, + const uint8_t *user_identity_id, + const uint8_t *friend_identity_id) +; + +/* + Add a DashPay external (watch-only) account with xpub bytes + + # Safety + - `wallet` must be valid, `xpub_bytes` must point to `xpub_len` bytes + - `user_identity_id` and `friend_identity_id` must each point to 32 bytes + */ + +FFIAccountResult wallet_add_dashpay_external_account_with_xpub_bytes(FFIWallet *wallet, + FFINetwork network, + unsigned int account_index, + const uint8_t *user_identity_id, + const uint8_t *friend_identity_id, + const uint8_t *xpub_bytes, + size_t xpub_len) +; + /* Add an account to the wallet with xpub as byte array diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index c0d18f38f..c1f99a23f 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -50,6 +50,15 @@ fn get_managed_account_by_type<'a>( AccountType::ProviderOwnerKeys => collection.provider_owner_keys.as_ref(), AccountType::ProviderOperatorKeys => collection.provider_operator_keys.as_ref(), AccountType::ProviderPlatformKeys => collection.provider_platform_keys.as_ref(), + AccountType::DashpayReceivingFunds { + .. + } + | AccountType::DashpayExternalAccount { + .. + } => { + // DashPay managed accounts are not currently persisted in ManagedAccountCollection + None + } } } @@ -84,6 +93,15 @@ fn get_managed_account_by_type_mut<'a>( AccountType::ProviderOwnerKeys => collection.provider_owner_keys.as_mut(), AccountType::ProviderOperatorKeys => collection.provider_operator_keys.as_mut(), AccountType::ProviderPlatformKeys => collection.provider_platform_keys.as_mut(), + AccountType::DashpayReceivingFunds { + .. + } + | AccountType::DashpayExternalAccount { + .. + } => { + // DashPay managed accounts are not currently persisted in ManagedAccountCollection + None + } } } @@ -783,6 +801,22 @@ pub unsafe extern "C" fn managed_wallet_mark_address_used( } } } + if !found { + for account in collection.dashpay_receival_accounts.values_mut() { + if account.mark_address_used(&address) { + found = true; + break; + } + } + } + if !found { + for account in collection.dashpay_external_accounts.values_mut() { + if account.mark_address_used(&address) { + found = true; + break; + } + } + } found }; diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index ac9394517..0be10f68f 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -13,6 +13,7 @@ use crate::address_pool::{FFIAddressPool, FFIAddressPoolType}; use crate::error::{FFIError, FFIErrorCode}; use crate::types::{FFIAccountType, FFINetwork}; use crate::wallet_manager::FFIWalletManager; +use key_wallet::account::account_collection::DashpayAccountKey; use key_wallet::managed_account::address_pool::AddressPool; use key_wallet::managed_account::ManagedAccount; use key_wallet::AccountType; @@ -171,6 +172,12 @@ pub unsafe extern "C" fn managed_wallet_get_account( AccountType::ProviderPlatformKeys => { managed_collection.provider_platform_keys.as_ref() } + AccountType::DashpayReceivingFunds { + .. + } => None, + AccountType::DashpayExternalAccount { + .. + } => None, }; match managed_account { @@ -292,6 +299,143 @@ pub unsafe extern "C" fn managed_wallet_get_top_up_account_with_registration_ind result } +/// Get a managed DashPay receiving funds account by composite key +/// +/// # Safety +/// - `manager`, `wallet_id` must be valid +/// - `user_identity_id` and `friend_identity_id` must each point to 32 bytes +#[no_mangle] +pub unsafe extern "C" fn managed_wallet_get_dashpay_receiving_account( + manager: *const FFIWalletManager, + wallet_id: *const u8, + network: FFINetwork, + account_index: c_uint, + user_identity_id: *const u8, + friend_identity_id: *const u8, +) -> FFIManagedAccountResult { + if manager.is_null() + || wallet_id.is_null() + || user_identity_id.is_null() + || friend_identity_id.is_null() + { + return FFIManagedAccountResult::error( + FFIErrorCode::InvalidInput, + "Null pointer provided".to_string(), + ); + } + let mut user_id = [0u8; 32]; + let mut friend_id = [0u8; 32]; + core::ptr::copy_nonoverlapping(user_identity_id, user_id.as_mut_ptr(), 32); + core::ptr::copy_nonoverlapping(friend_identity_id, friend_id.as_mut_ptr(), 32); + let key = DashpayAccountKey { + index: account_index, + user_identity_id: user_id, + friend_identity_id: friend_id, + }; + + let mut error = FFIError::success(); + let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( + manager, wallet_id, &mut error, + ); + if managed_wallet_ptr.is_null() { + return FFIManagedAccountResult::error( + error.code, + if error.message.is_null() { + "Failed to get managed wallet info".to_string() + } else { + std::ffi::CStr::from_ptr(error.message).to_string_lossy().to_string() + }, + ); + } + let network_rust: key_wallet::Network = network.into(); + let managed_wallet = &*managed_wallet_ptr; + let result = match managed_wallet.inner().accounts.get(&network_rust) { + Some(coll) => match coll.dashpay_receival_accounts.get(&key) { + Some(account) => FFIManagedAccountResult::success(Box::into_raw(Box::new( + FFIManagedAccount::new(account), + ))), + None => FFIManagedAccountResult::error( + FFIErrorCode::NotFound, + "Account not found".to_string(), + ), + }, + None => FFIManagedAccountResult::error( + FFIErrorCode::NotFound, + "No accounts for network".to_string(), + ), + }; + crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); + result +} + +/// Get a managed DashPay external account by composite key +/// +/// # Safety +/// - Pointers must be valid +#[no_mangle] +pub unsafe extern "C" fn managed_wallet_get_dashpay_external_account( + manager: *const FFIWalletManager, + wallet_id: *const u8, + network: FFINetwork, + account_index: c_uint, + user_identity_id: *const u8, + friend_identity_id: *const u8, +) -> FFIManagedAccountResult { + if manager.is_null() + || wallet_id.is_null() + || user_identity_id.is_null() + || friend_identity_id.is_null() + { + return FFIManagedAccountResult::error( + FFIErrorCode::InvalidInput, + "Null pointer provided".to_string(), + ); + } + let mut user_id = [0u8; 32]; + let mut friend_id = [0u8; 32]; + core::ptr::copy_nonoverlapping(user_identity_id, user_id.as_mut_ptr(), 32); + core::ptr::copy_nonoverlapping(friend_identity_id, friend_id.as_mut_ptr(), 32); + let key = DashpayAccountKey { + index: account_index, + user_identity_id: user_id, + friend_identity_id: friend_id, + }; + + let mut error = FFIError::success(); + let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( + manager, wallet_id, &mut error, + ); + if managed_wallet_ptr.is_null() { + return FFIManagedAccountResult::error( + error.code, + if error.message.is_null() { + "Failed to get managed wallet info".to_string() + } else { + std::ffi::CStr::from_ptr(error.message).to_string_lossy().to_string() + }, + ); + } + let network_rust: key_wallet::Network = network.into(); + let managed_wallet = &*managed_wallet_ptr; + let result = match managed_wallet.inner().accounts.get(&network_rust) { + Some(coll) => match coll.dashpay_external_accounts.get(&key) { + Some(account) => FFIManagedAccountResult::success(Box::into_raw(Box::new( + FFIManagedAccount::new(account), + ))), + None => FFIManagedAccountResult::error( + FFIErrorCode::NotFound, + "Account not found".to_string(), + ), + }, + None => FFIManagedAccountResult::error( + FFIErrorCode::NotFound, + "No accounts for network".to_string(), + ), + }; + crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); + result +} + /// Get the network of a managed account /// /// # Safety @@ -377,6 +521,12 @@ pub unsafe extern "C" fn managed_account_get_account_type( AccountType::ProviderOwnerKeys => FFIAccountType::ProviderOwnerKeys, AccountType::ProviderOperatorKeys => FFIAccountType::ProviderOperatorKeys, AccountType::ProviderPlatformKeys => FFIAccountType::ProviderPlatformKeys, + AccountType::DashpayReceivingFunds { + .. + } => FFIAccountType::DashpayReceivingFunds, + AccountType::DashpayExternalAccount { + .. + } => FFIAccountType::DashpayExternalAccount, } } @@ -852,6 +1002,14 @@ pub unsafe extern "C" fn managed_account_get_address_pool( ManagedAccountType::ProviderPlatformKeys { addresses, } => addresses, + ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } => addresses, + ManagedAccountType::DashpayExternalAccount { + addresses, + .. + } => addresses, }; let ffi_pool = FFIAddressPool { diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index aa03fb609..b9895afdd 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -552,13 +552,20 @@ pub unsafe extern "C" fn wallet_check_transaction( let mut managed_info = ManagedWalletInfo::from_wallet(wallet.inner()); - // Check the transaction - let wallet_opt = if update_state { - Some(wallet.inner()) - } else { - None + // Check the transaction - wallet is always required now + let wallet_mut = match wallet.inner_mut() { + Some(w) => w, + None => { + FFIError::set_error( + error, + FFIErrorCode::InternalError, + "Cannot get mutable wallet reference (Arc has multiple owners)".to_string(), + ); + return false; + } }; - let check_result = managed_info.check_transaction(&tx, network_rust, context, wallet_opt); + let check_result = + managed_info.check_transaction(&tx, network_rust, context, wallet_mut, update_state); // If we updated state, we need to update the wallet's managed info // Note: This would require storing ManagedWalletInfo in FFIWallet diff --git a/key-wallet-ffi/src/transaction_checking.rs b/key-wallet-ffi/src/transaction_checking.rs index 1b2ebd20d..1ce0e4d89 100644 --- a/key-wallet-ffi/src/transaction_checking.rs +++ b/key-wallet-ffi/src/transaction_checking.rs @@ -100,7 +100,7 @@ pub unsafe extern "C" fn wallet_create_managed_wallet( /// # Safety /// /// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `wallet` must be a valid pointer to an FFIWallet (needed for address generation) +/// - `wallet` must be a valid pointer to an FFIWallet (needed for address generation and DashPay queries) /// - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes /// - `result_out` must be a valid pointer to store the result /// - `error` must be a valid pointer to an FFIError @@ -108,7 +108,7 @@ pub unsafe extern "C" fn wallet_create_managed_wallet( #[no_mangle] pub unsafe extern "C" fn managed_wallet_check_transaction( managed_wallet: *mut FFIManagedWalletInfo, - wallet: *const FFIWallet, + wallet: *mut FFIWallet, network: FFINetwork, tx_bytes: *const u8, tx_len: usize, @@ -187,15 +187,29 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( } }; - // Check the transaction - let update_wallet = if update_state && !wallet.is_null() { - Some(&(*wallet).inner()) - } else { - None - }; + // Check the transaction - wallet is now required + if wallet.is_null() { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Wallet pointer is required".to_string(), + ); + return false; + } + let wallet_mut = match (*wallet).inner_mut() { + Some(w) => w, + None => { + FFIError::set_error( + error, + FFIErrorCode::InternalError, + "Cannot get mutable wallet reference (Arc has multiple owners)".to_string(), + ); + return false; + } + }; let check_result = - managed_wallet.check_transaction(&tx, network_rust, context, update_wallet.copied()); + managed_wallet.check_transaction(&tx, network_rust, context, wallet_mut, update_state); // Convert the result to FFI format let affected_accounts = if check_result.affected_accounts.is_empty() { @@ -402,6 +416,42 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( ffi_accounts.push(ffi_match); continue; } + AccountTypeMatch::DashpayReceivingFunds { + account_index, + involved_addresses, + } => { + let ffi_match = FFIAccountMatch { + account_type: 11, // DashpayReceivingFunds + 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; + } + AccountTypeMatch::DashpayExternalAccount { + account_index, + involved_addresses, + } => { + let ffi_match = FFIAccountMatch { + account_type: 12, // DashpayExternalAccount + 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 8abef7252..3dcb0f48e 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -264,6 +264,8 @@ pub enum FFIAccountType { ProviderOperatorKeys = 9, /// Provider platform P2P keys (DIP-3, ED25519) - Path: m/9'/5'/3'/4'/[key_index] ProviderPlatformKeys = 10, + DashpayReceivingFunds = 11, + DashpayExternalAccount = 12, } impl FFIAccountType { @@ -298,10 +300,48 @@ impl FFIAccountType { FFIAccountType::ProviderOwnerKeys => key_wallet::AccountType::ProviderOwnerKeys, FFIAccountType::ProviderOperatorKeys => key_wallet::AccountType::ProviderOperatorKeys, FFIAccountType::ProviderPlatformKeys => key_wallet::AccountType::ProviderPlatformKeys, + // DashPay variants require additional identity IDs (user_identity_id and friend_identity_id) + // that are not part of the current FFI API. These types cannot be constructed via this + // conversion path. Attempting to use them is a programming error. + // + // TODO: Extend the FFI API to accept identity IDs for DashPay account creation: + // - Add new FFI functions like: + // * ffi_account_type_to_dashpay_receiving_funds(index, user_id[32], friend_id[32]) + // * ffi_account_type_to_dashpay_external_account(index, user_id[32], friend_id[32]) + // - Or extend to_account_type to accept optional identity ID parameters + // + // Until then, attempting to convert these variants will panic to prevent silent misrouting. + FFIAccountType::DashpayReceivingFunds => { + panic!( + "FFIAccountType::DashpayReceivingFunds cannot be converted to AccountType \ + without user_identity_id and friend_identity_id. The FFI API does not yet \ + support passing these 32-byte identity IDs. This is a programming error - \ + DashPay account creation must use a different API path." + ); + } + FFIAccountType::DashpayExternalAccount => { + panic!( + "FFIAccountType::DashpayExternalAccount cannot be converted to AccountType \ + without user_identity_id and friend_identity_id. The FFI API does not yet \ + support passing these 32-byte identity IDs. This is a programming error - \ + DashPay account creation must use a different API path." + ); + } } } - /// Convert from AccountType + /// Convert from AccountType to FFI representation + /// + /// Returns: (FFIAccountType, primary_index, optional_secondary_index) + /// + /// # Panics + /// + /// Panics when attempting to convert DashPay account types (DashpayReceivingFunds, + /// DashpayExternalAccount) because they contain 32-byte identity IDs that cannot be + /// represented in the current FFI tuple format. This prevents silent data loss. + /// + /// TODO: Extend the return type or create separate FFI functions that can return + /// the full DashPay account information including identity IDs. pub fn from_account_type(account_type: &key_wallet::AccountType) -> (Self, u32, Option) { use key_wallet::account::account_type::StandardAccountType; match account_type { @@ -339,10 +379,111 @@ impl FFIAccountType { key_wallet::AccountType::ProviderPlatformKeys => { (FFIAccountType::ProviderPlatformKeys, 0, None) } + key_wallet::AccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + } => { + // Cannot convert DashPay accounts to FFI without losing identity ID information + panic!( + "Cannot convert AccountType::DashpayReceivingFunds (index={}, user_id={:?}, friend_id={:?}) \ + to FFI representation. The current FFI tuple format (FFIAccountType, u32, Option) \ + cannot represent the two 32-byte identity IDs required by DashPay accounts. \ + This would result in silent data loss. A dedicated FFI API for DashPay accounts is needed.", + index, + &user_identity_id[..8], // Show first 8 bytes for debugging + &friend_identity_id[..8] + ); + } + key_wallet::AccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + } => { + // Cannot convert DashPay accounts to FFI without losing identity ID information + panic!( + "Cannot convert AccountType::DashpayExternalAccount (index={}, user_id={:?}, friend_id={:?}) \ + to FFI representation. The current FFI tuple format (FFIAccountType, u32, Option) \ + cannot represent the two 32-byte identity IDs required by DashPay accounts. \ + This would result in silent data loss. A dedicated FFI API for DashPay accounts is needed.", + index, + &user_identity_id[..8], // Show first 8 bytes for debugging + &friend_identity_id[..8] + ); + } } } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "DashpayReceivingFunds cannot be converted to AccountType")] + fn test_dashpay_receiving_funds_to_account_type_panics() { + // This should panic because we cannot construct a DashPay account without identity IDs + let _ = FFIAccountType::DashpayReceivingFunds.to_account_type(0); + } + + #[test] + #[should_panic(expected = "DashpayExternalAccount cannot be converted to AccountType")] + fn test_dashpay_external_account_to_account_type_panics() { + // This should panic because we cannot construct a DashPay account without identity IDs + let _ = FFIAccountType::DashpayExternalAccount.to_account_type(0); + } + + #[test] + #[should_panic(expected = "Cannot convert AccountType::DashpayReceivingFunds")] + fn test_dashpay_receiving_funds_from_account_type_panics() { + // This should panic because we cannot represent identity IDs in the FFI tuple + let account_type = key_wallet::AccountType::DashpayReceivingFunds { + index: 0, + user_identity_id: [1u8; 32], + friend_identity_id: [2u8; 32], + }; + let _ = FFIAccountType::from_account_type(&account_type); + } + + #[test] + #[should_panic(expected = "Cannot convert AccountType::DashpayExternalAccount")] + fn test_dashpay_external_account_from_account_type_panics() { + // This should panic because we cannot represent identity IDs in the FFI tuple + let account_type = key_wallet::AccountType::DashpayExternalAccount { + index: 0, + user_identity_id: [1u8; 32], + friend_identity_id: [2u8; 32], + }; + let _ = FFIAccountType::from_account_type(&account_type); + } + + #[test] + fn test_non_dashpay_conversions_work() { + // Verify that non-DashPay types still convert correctly + let standard_bip44 = FFIAccountType::StandardBIP44.to_account_type(5); + assert!(matches!( + standard_bip44, + key_wallet::AccountType::Standard { + index: 5, + .. + } + )); + + let coinjoin = FFIAccountType::CoinJoin.to_account_type(3); + assert!(matches!( + coinjoin, + key_wallet::AccountType::CoinJoin { + index: 3 + } + )); + + // Test reverse conversion + let (ffi_type, index, _) = FFIAccountType::from_account_type(&standard_bip44); + assert_eq!(ffi_type, FFIAccountType::StandardBIP44); + assert_eq!(index, 5); + } +} + /// Address type enumeration #[repr(C)] #[derive(Debug, Clone, Copy)] diff --git a/key-wallet-ffi/src/wallet.rs b/key-wallet-ffi/src/wallet.rs index 59d3f16cb..d0f5aa315 100644 --- a/key-wallet-ffi/src/wallet.rs +++ b/key-wallet-ffi/src/wallet.rs @@ -9,6 +9,7 @@ use std::os::raw::{c_char, c_uint}; use std::ptr; use std::slice; +use crate::types::FFIAccountResult; use key_wallet::wallet::initialization::WalletAccountCreationOptions; use key_wallet::{Mnemonic, Network, Seed, Wallet}; @@ -521,6 +522,138 @@ pub unsafe extern "C" fn wallet_add_account( } } +/// Add a DashPay receiving funds account +/// +/// # Safety +/// - `wallet` must be a valid pointer +/// - `user_identity_id` and `friend_identity_id` must each point to 32 bytes +#[no_mangle] +pub unsafe extern "C" fn wallet_add_dashpay_receiving_account( + wallet: *mut FFIWallet, + network: FFINetwork, + account_index: c_uint, + user_identity_id: *const u8, + friend_identity_id: *const u8, +) -> FFIAccountResult { + use key_wallet::account::AccountType; + if wallet.is_null() || user_identity_id.is_null() || friend_identity_id.is_null() { + return FFIAccountResult::error( + crate::error::FFIErrorCode::InvalidInput, + "Null pointer provided".to_string(), + ); + } + let w = &mut *wallet; + let wallet_mut = match w.inner_mut() { + Some(w) => w, + None => { + return FFIAccountResult::error( + crate::error::FFIErrorCode::InvalidInput, + "Wallet is immutable".to_string(), + ) + } + }; + let mut user_id = [0u8; 32]; + let mut friend_id = [0u8; 32]; + core::ptr::copy_nonoverlapping(user_identity_id, user_id.as_mut_ptr(), 32); + core::ptr::copy_nonoverlapping(friend_identity_id, friend_id.as_mut_ptr(), 32); + + let acct = AccountType::DashpayReceivingFunds { + index: account_index, + user_identity_id: user_id, + friend_identity_id: friend_id, + }; + let network_rust: key_wallet::Network = network.into(); + match wallet_mut.add_account(acct, network_rust, None) { + Ok(()) => { + if let Some(coll) = wallet_mut.accounts.get(&network_rust) { + if let Some(account) = coll.account_of_type(acct) { + let ffi_account = crate::types::FFIAccount::new(account); + return FFIAccountResult::success(Box::into_raw(Box::new(ffi_account))); + } + } + FFIAccountResult::error( + crate::error::FFIErrorCode::WalletError, + "Failed to retrieve account after adding".to_string(), + ) + } + Err(e) => FFIAccountResult::error(crate::error::FFIErrorCode::InvalidInput, e.to_string()), + } +} + +/// Add a DashPay external (watch-only) account with xpub bytes +/// +/// # Safety +/// - `wallet` must be valid, `xpub_bytes` must point to `xpub_len` bytes +/// - `user_identity_id` and `friend_identity_id` must each point to 32 bytes +#[no_mangle] +pub unsafe extern "C" fn wallet_add_dashpay_external_account_with_xpub_bytes( + wallet: *mut FFIWallet, + network: FFINetwork, + account_index: c_uint, + user_identity_id: *const u8, + friend_identity_id: *const u8, + xpub_bytes: *const u8, + xpub_len: usize, +) -> FFIAccountResult { + use key_wallet::account::AccountType; + use key_wallet::bip32::ExtendedPubKey; + if wallet.is_null() + || user_identity_id.is_null() + || friend_identity_id.is_null() + || xpub_bytes.is_null() + { + return FFIAccountResult::error( + crate::error::FFIErrorCode::InvalidInput, + "Null pointer provided".to_string(), + ); + } + let w = &mut *wallet; + let wallet_mut = match w.inner_mut() { + Some(w) => w, + None => { + return FFIAccountResult::error( + crate::error::FFIErrorCode::InvalidInput, + "Wallet is immutable".to_string(), + ) + } + }; + let mut user_id = [0u8; 32]; + let mut friend_id = [0u8; 32]; + core::ptr::copy_nonoverlapping(user_identity_id, user_id.as_mut_ptr(), 32); + core::ptr::copy_nonoverlapping(friend_identity_id, friend_id.as_mut_ptr(), 32); + let xpub_slice = core::slice::from_raw_parts(xpub_bytes, xpub_len); + let xpub = match ExtendedPubKey::decode(xpub_slice) { + Ok(x) => x, + Err(_) => { + return FFIAccountResult::error( + crate::error::FFIErrorCode::InvalidInput, + "Invalid xpub bytes".to_string(), + ) + } + }; + let acct = AccountType::DashpayExternalAccount { + index: account_index, + user_identity_id: user_id, + friend_identity_id: friend_id, + }; + let network_rust: key_wallet::Network = network.into(); + match wallet_mut.add_account(acct, network_rust, Some(xpub)) { + Ok(()) => { + if let Some(coll) = wallet_mut.accounts.get(&network_rust) { + if let Some(account) = coll.account_of_type(acct) { + let ffi_account = crate::types::FFIAccount::new(account); + return FFIAccountResult::success(Box::into_raw(Box::new(ffi_account))); + } + } + FFIAccountResult::error( + crate::error::FFIErrorCode::WalletError, + "Failed to retrieve account after adding".to_string(), + ) + } + Err(e) => FFIAccountResult::error(crate::error::FFIErrorCode::InvalidInput, e.to_string()), + } +} + /// Add an account to the wallet with xpub as byte array /// /// # Safety diff --git a/key-wallet-manager/src/wallet_manager/mod.rs b/key-wallet-manager/src/wallet_manager/mod.rs index 143b890c1..452d15b00 100644 --- a/key-wallet-manager/src/wallet_manager/mod.rs +++ b/key-wallet-manager/src/wallet_manager/mod.rs @@ -515,19 +515,24 @@ impl WalletManager { let wallet_ids: Vec = self.wallets.keys().cloned().collect(); for wallet_id in wallet_ids { - // Check the transaction for this wallet - if let Some(wallet_info) = self.wallet_infos.get_mut(&wallet_id) { + // Get mutable references to both wallet and wallet_info + // We need to use split borrowing to get around Rust's borrow checker + let wallet_opt = self.wallets.get_mut(&wallet_id); + let wallet_info_opt = self.wallet_infos.get_mut(&wallet_id); + + if let (Some(wallet), Some(wallet_info)) = (wallet_opt, wallet_info_opt) { let result = wallet_info.check_transaction( tx, network, context, - self.wallets.get(&wallet_id), + wallet, + update_state_if_found, ); // If the transaction is relevant if result.is_relevant { relevant_wallets.push(wallet_id); - // Note: balance update is already handled in check_transaction when update_state_if_found is true + // Note: balance update is already handled in check_transaction } } } diff --git a/key-wallet/src/account/account_collection.rs b/key-wallet/src/account/account_collection.rs index 7702fdf6a..b84aea6cf 100644 --- a/key-wallet/src/account/account_collection.rs +++ b/key-wallet/src/account/account_collection.rs @@ -16,6 +16,18 @@ use crate::account::BLSAccount; use crate::account::EdDSAAccount; use crate::AccountType; +pub type DashpayOurUserIdentityId = [u8; 32]; +pub type DashpayContactIdentityId = [u8; 32]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +pub struct DashpayAccountKey { + pub index: u32, + pub user_identity_id: DashpayOurUserIdentityId, + pub friend_identity_id: DashpayContactIdentityId, +} + /// Collection of accounts organized by type #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -45,6 +57,10 @@ pub struct AccountCollection { /// Provider platform keys (optional) #[cfg(feature = "eddsa")] pub provider_platform_keys: Option, + /// DashPay receiving funds accounts + pub dashpay_receival_accounts: BTreeMap, + /// DashPay external (watch-only) accounts + pub dashpay_external_accounts: BTreeMap, } impl AccountCollection { @@ -64,6 +80,8 @@ impl AccountCollection { provider_operator_keys: None, #[cfg(feature = "eddsa")] provider_platform_keys: None, + dashpay_receival_accounts: BTreeMap::new(), + dashpay_external_accounts: BTreeMap::new(), } } @@ -115,6 +133,30 @@ impl AccountCollection { AccountType::ProviderPlatformKeys => { return Err("ProviderPlatformKeys requires EdDSAAccount, use insert_eddsa_account"); } + AccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_receival_accounts.insert(key, account); + } + AccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_external_accounts.insert(key, account); + } } Ok(()) } @@ -174,6 +216,30 @@ impl AccountCollection { AccountType::ProviderPlatformKeys => self.provider_platform_keys.is_some(), #[cfg(not(feature = "eddsa"))] AccountType::ProviderPlatformKeys => false, + AccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_receival_accounts.contains_key(&key) + } + AccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_external_accounts.contains_key(&key) + } } } @@ -203,6 +269,30 @@ impl AccountCollection { AccountType::ProviderOwnerKeys => self.provider_owner_keys.as_ref(), AccountType::ProviderOperatorKeys => None, // BLSAccount, use bls_account_of_type AccountType::ProviderPlatformKeys => None, // EdDSAAccount, use eddsa_account_of_type + AccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + } => { + let key = DashpayAccountKey { + index, + user_identity_id, + friend_identity_id, + }; + self.dashpay_receival_accounts.get(&key) + } + AccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + } => { + let key = DashpayAccountKey { + index, + user_identity_id, + friend_identity_id, + }; + self.dashpay_external_accounts.get(&key) + } } } @@ -232,6 +322,30 @@ impl AccountCollection { AccountType::ProviderOwnerKeys => self.provider_owner_keys.as_mut(), AccountType::ProviderOperatorKeys => None, // BLSAccount, use bls_account_of_type_mut AccountType::ProviderPlatformKeys => None, // EdDSAAccount, use eddsa_account_of_type_mut + AccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + } => { + let key = DashpayAccountKey { + index, + user_identity_id, + friend_identity_id, + }; + self.dashpay_receival_accounts.get_mut(&key) + } + AccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + } => { + let key = DashpayAccountKey { + index, + user_identity_id, + friend_identity_id, + }; + self.dashpay_external_accounts.get_mut(&key) + } } } diff --git a/key-wallet/src/account/account_type.rs b/key-wallet/src/account/account_type.rs index 7bae610f7..10056de42 100644 --- a/key-wallet/src/account/account_type.rs +++ b/key-wallet/src/account/account_type.rs @@ -63,6 +63,26 @@ pub enum AccountType { /// Provider platform P2P keys (DIP-3, ED25519) /// Path: m/9'/5'/3'/4'/[key_index] ProviderPlatformKeys, + /// Incoming DashPay funds account using 256-bit derivation + /// The derivation path used is user_identity_id/friend_identity_id + DashpayReceivingFunds { + /// Account index (account-level selection) + index: u32, + /// Our identity id (32 bytes) + user_identity_id: [u8; 32], + /// Our contact's identity id (32 bytes) + friend_identity_id: [u8; 32], + }, + /// DashPay external (watch-only) account using 256-bit derivation + /// The derivation path used is friend_identity_id/user_identity_id + DashpayExternalAccount { + /// Account index (account-level selection) + index: u32, + /// Our identity id (32 bytes) + user_identity_id: [u8; 32], + /// Our contact's identity id (32 bytes) + friend_identity_id: [u8; 32], + }, } impl From for AccountTypeToCheck { @@ -90,6 +110,12 @@ impl From for AccountTypeToCheck { AccountType::ProviderOwnerKeys => AccountTypeToCheck::ProviderOwnerKeys, AccountType::ProviderOperatorKeys => AccountTypeToCheck::ProviderOperatorKeys, AccountType::ProviderPlatformKeys => AccountTypeToCheck::ProviderPlatformKeys, + AccountType::DashpayReceivingFunds { + .. + } => AccountTypeToCheck::DashpayReceivingFunds, + AccountType::DashpayExternalAccount { + .. + } => AccountTypeToCheck::DashpayExternalAccount, } } } @@ -105,6 +131,14 @@ impl AccountType { } | Self::CoinJoin { index, + } + | Self::DashpayReceivingFunds { + index, + .. + } + | Self::DashpayExternalAccount { + index, + .. } => Some(*index), // Identity and provider types don't have account indices Self::IdentityRegistration @@ -168,6 +202,12 @@ impl AccountType { Self::ProviderPlatformKeys { .. } => DerivationPathReference::ProviderPlatformNodeKeys, + Self::DashpayReceivingFunds { + .. + } => DerivationPathReference::ContactBasedFunds, + Self::DashpayExternalAccount { + .. + } => DerivationPathReference::ContactBasedFundsExternal, } } @@ -306,6 +346,50 @@ impl AccountType { ChildNumber::from_hardened_idx(4).map_err(crate::error::Error::Bip32)?, ])) } + Self::DashpayReceivingFunds { + user_identity_id, + friend_identity_id, + .. + } => { + // Base DashPay root + account 0' + user_id/friend_id (non-hardened per DIP-14/DIP-15) + let mut path = match network { + Network::Dash => DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_MAINNET), + Network::Testnet => { + DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_TESTNET) + } + _ => return Err(crate::error::Error::InvalidNetwork), + }; + path.push(ChildNumber::from_hardened_idx(0).map_err(crate::error::Error::Bip32)?); + path.push(ChildNumber::Normal256 { + index: *user_identity_id, + }); + path.push(ChildNumber::Normal256 { + index: *friend_identity_id, + }); + Ok(path) + } + Self::DashpayExternalAccount { + user_identity_id, + friend_identity_id, + .. + } => { + // Base DashPay root + account 0' + friend_id/user_id (non-hardened per DIP-14/DIP-15) + let mut path = match network { + Network::Dash => DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_MAINNET), + Network::Testnet => { + DerivationPath::from(crate::dip9::DASHPAY_ROOT_PATH_TESTNET) + } + _ => return Err(crate::error::Error::InvalidNetwork), + }; + path.push(ChildNumber::from_hardened_idx(0).map_err(crate::error::Error::Bip32)?); + path.push(ChildNumber::Normal256 { + index: *friend_identity_id, + }); + path.push(ChildNumber::Normal256 { + index: *user_identity_id, + }); + Ok(path) + } } } } diff --git a/key-wallet/src/dip9.rs b/key-wallet/src/dip9.rs index 514c96cec..d1b279359 100644 --- a/key-wallet/src/dip9.rs +++ b/key-wallet/src/dip9.rs @@ -156,6 +156,39 @@ pub const DASH_BIP44_PATH_TESTNET: IndexConstPath<2> = IndexConstPath { reference: DerivationPathReference::BIP44, path_type: DerivationPathType::CLEAR_FUNDS, }; + +// DashPay Root Paths +pub const DASHPAY_ROOT_PATH_MAINNET: IndexConstPath<3> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_DASHPAY, + }, + ], + reference: DerivationPathReference::ContactBasedFunds, + path_type: DerivationPathType::CLEAR_FUNDS, +}; + +pub const DASHPAY_ROOT_PATH_TESTNET: IndexConstPath<3> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_TESTNET_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_DASHPAY, + }, + ], + reference: DerivationPathReference::ContactBasedFunds, + path_type: DerivationPathType::CLEAR_FUNDS, +}; // CoinJoin Paths pub const COINJOIN_PATH_MAINNET: IndexConstPath<3> = IndexConstPath { diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index d4a22391b..85e1614a4 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -3,6 +3,7 @@ //! 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_type::AccountType; use crate::gap_limit::{ DEFAULT_COINJOIN_GAP_LIMIT, DEFAULT_EXTERNAL_GAP_LIMIT, DEFAULT_INTERNAL_GAP_LIMIT, @@ -44,6 +45,10 @@ pub struct ManagedAccountCollection { pub provider_operator_keys: Option, /// Provider platform keys (optional) pub provider_platform_keys: Option, + /// DashPay receiving funds accounts keyed by (index, user_id, friend_id) + pub dashpay_receival_accounts: BTreeMap, + /// DashPay external accounts keyed by (index, user_id, friend_id) + pub dashpay_external_accounts: BTreeMap, } impl ManagedAccountCollection { @@ -61,6 +66,8 @@ impl ManagedAccountCollection { provider_owner_keys: None, provider_operator_keys: None, provider_platform_keys: None, + dashpay_receival_accounts: BTreeMap::new(), + dashpay_external_accounts: BTreeMap::new(), } } @@ -110,6 +117,32 @@ impl ManagedAccountCollection { ManagedAccountType::ProviderPlatformKeys { .. } => self.provider_platform_keys.is_some(), + ManagedAccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + .. + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_receival_accounts.contains_key(&key) + } + ManagedAccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + .. + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_external_accounts.contains_key(&key) + } } } @@ -177,6 +210,32 @@ impl ManagedAccountCollection { } => { self.provider_platform_keys = Some(account); } + ManagedAccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + .. + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_receival_accounts.insert(key, account); + } + ManagedAccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + .. + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_external_accounts.insert(key, account); + } } } @@ -259,6 +318,20 @@ impl ManagedAccountCollection { } } + // Convert DashPay receiving accounts + for (key, account) in &account_collection.dashpay_receival_accounts { + if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + managed_collection.dashpay_receival_accounts.insert(*key, managed_account); + } + } + + // Convert DashPay external accounts + for (key, account) in &account_collection.dashpay_external_accounts { + if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + managed_collection.dashpay_external_accounts.insert(*key, managed_account); + } + } + managed_collection } @@ -471,6 +544,34 @@ impl ManagedAccountCollection { addresses, } } + AccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + } => { + let addresses = + AddressPool::new(base_path, AddressPoolType::Absent, 20, network, key_source)?; + ManagedAccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + addresses, + } + } + AccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + } => { + let addresses = + AddressPool::new(base_path, AddressPoolType::Absent, 20, network, key_source)?; + ManagedAccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + addresses, + } + } }; Ok(ManagedAccount::new(managed_type, network, is_watch_only)) @@ -619,6 +720,10 @@ impl ManagedAccountCollection { accounts.push(account); } + // Add DashPay accounts + accounts.extend(self.dashpay_receival_accounts.values()); + accounts.extend(self.dashpay_external_accounts.values()); + accounts } @@ -666,6 +771,10 @@ impl ManagedAccountCollection { accounts.push(account); } + // Add DashPay accounts + accounts.extend(self.dashpay_receival_accounts.values_mut()); + accounts.extend(self.dashpay_external_accounts.values_mut()); + accounts } @@ -706,6 +815,8 @@ impl ManagedAccountCollection { && self.provider_owner_keys.is_none() && self.provider_operator_keys.is_none() && self.provider_platform_keys.is_none() + && self.dashpay_receival_accounts.is_empty() + && self.dashpay_external_accounts.is_empty() } /// Clear all accounts @@ -721,5 +832,7 @@ impl ManagedAccountCollection { self.provider_owner_keys = None; self.provider_operator_keys = None; self.provider_platform_keys = None; + self.dashpay_receival_accounts.clear(); + self.dashpay_external_accounts.clear(); } } diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index f6b132f15..8dbc2e805 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -1,3 +1,4 @@ +use crate::account::account_collection::{DashpayContactIdentityId, DashpayOurUserIdentityId}; use crate::account::StandardAccountType; use crate::gap_limit::{ DEFAULT_COINJOIN_GAP_LIMIT, DEFAULT_EXTERNAL_GAP_LIMIT, DEFAULT_INTERNAL_GAP_LIMIT, @@ -81,6 +82,28 @@ pub enum ManagedAccountType { /// Provider platform keys address pool addresses: AddressPool, }, + /// DashPay receiving funds account (single-pool) + DashpayReceivingFunds { + /// Account index + index: u32, + /// Our identity id + user_identity_id: DashpayOurUserIdentityId, + /// Contact identity id + friend_identity_id: DashpayContactIdentityId, + /// Address pool + addresses: AddressPool, + }, + /// DashPay external (watch-only) account (single-pool) + DashpayExternalAccount { + /// Account index + index: u32, + /// Our identity id + user_identity_id: DashpayOurUserIdentityId, + /// Contact identity id + friend_identity_id: DashpayContactIdentityId, + /// Address pool + addresses: AddressPool, + }, } impl ManagedAccountType { @@ -121,6 +144,14 @@ impl ManagedAccountType { | Self::ProviderPlatformKeys { .. } => None, + Self::DashpayReceivingFunds { + index, + .. + } + | Self::DashpayExternalAccount { + index, + .. + } => Some(*index), } } @@ -188,6 +219,14 @@ impl ManagedAccountType { } => { vec![addresses] } + Self::DashpayReceivingFunds { + addresses, + .. + } + | Self::DashpayExternalAccount { + addresses, + .. + } => vec![addresses], } } @@ -239,6 +278,14 @@ impl ManagedAccountType { } => { vec![addresses] } + Self::DashpayReceivingFunds { + addresses, + .. + } + | Self::DashpayExternalAccount { + addresses, + .. + } => vec![addresses], } } @@ -334,6 +381,26 @@ impl ManagedAccountType { Self::ProviderPlatformKeys { .. } => AccountType::ProviderPlatformKeys, + Self::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + .. + } => AccountType::DashpayReceivingFunds { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }, + Self::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + .. + } => AccountType::DashpayExternalAccount { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }, } } @@ -533,6 +600,50 @@ impl ManagedAccountType { addresses: pool, }) } + AccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + } => { + let path = account_type + .derivation_path(network) + .unwrap_or_else(|_| DerivationPath::master()); + let pool = AddressPool::new( + path, + crate::managed_account::address_pool::AddressPoolType::Absent, + 20, + network, + key_source, + )?; + Ok(Self::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + addresses: pool, + }) + } + AccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + } => { + let path = account_type + .derivation_path(network) + .unwrap_or_else(|_| DerivationPath::master()); + let pool = AddressPool::new( + path, + crate::managed_account::address_pool::AddressPoolType::Absent, + 20, + network, + key_source, + )?; + Ok(Self::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + addresses: pool, + }) + } } } } diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index 0769bf7fe..a55248a36 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -236,6 +236,14 @@ impl ManagedAccount { | ManagedAccountType::ProviderPlatformKeys { addresses, .. + } + | ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } + | ManagedAccountType::DashpayExternalAccount { + addresses, + .. } => { addresses.unused_addresses().first().and_then(|addr| addresses.address_index(addr)) } @@ -476,6 +484,14 @@ impl ManagedAccount { | ManagedAccountType::ProviderPlatformKeys { addresses, .. + } + | ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } + | ManagedAccountType::DashpayExternalAccount { + addresses, + .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -553,6 +569,14 @@ impl ManagedAccount { | ManagedAccountType::ProviderPlatformKeys { addresses, .. + } + | ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } + | ManagedAccountType::DashpayExternalAccount { + addresses, + .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -770,6 +794,14 @@ impl ManagedAccount { | ManagedAccountType::ProviderPlatformKeys { addresses, .. + } + | ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } + | ManagedAccountType::DashpayExternalAccount { + 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 6b4b967ee..9048bdf0b 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -93,6 +93,16 @@ pub enum AccountTypeMatch { ProviderPlatformKeys { involved_addresses: Vec, }, + /// DashPay receiving funds account (single-pool) + DashpayReceivingFunds { + account_index: u32, + involved_addresses: Vec, + }, + /// DashPay external account (single-pool) + DashpayExternalAccount { + account_index: u32, + involved_addresses: Vec, + }, } impl AccountTypeMatch { @@ -142,6 +152,14 @@ impl AccountTypeMatch { | AccountTypeMatch::ProviderPlatformKeys { involved_addresses, } => involved_addresses.clone(), + AccountTypeMatch::DashpayReceivingFunds { + involved_addresses, + .. + } + | AccountTypeMatch::DashpayExternalAccount { + involved_addresses, + .. + } => involved_addresses.clone(), } } @@ -164,6 +182,14 @@ impl AccountTypeMatch { account_index, .. } => Some(*account_index), + AccountTypeMatch::DashpayReceivingFunds { + account_index, + .. + } + | AccountTypeMatch::DashpayExternalAccount { + account_index, + .. + } => Some(*account_index), _ => None, } } @@ -204,6 +230,12 @@ impl AccountTypeMatch { AccountTypeMatch::ProviderPlatformKeys { .. } => AccountTypeToCheck::ProviderPlatformKeys, + AccountTypeMatch::DashpayReceivingFunds { + .. + } => AccountTypeToCheck::DashpayReceivingFunds, + AccountTypeMatch::DashpayExternalAccount { + .. + } => AccountTypeToCheck::DashpayExternalAccount, } } } @@ -316,6 +348,24 @@ impl ManagedAccountCollection { }) .into_iter() .collect(), + AccountTypeToCheck::DashpayReceivingFunds => { + let mut matches = Vec::new(); + for (key, account) in &self.dashpay_receival_accounts { + if let Some(m) = account.check_transaction_for_match(tx, Some(key.index)) { + matches.push(m); + } + } + matches + } + AccountTypeToCheck::DashpayExternalAccount => { + let mut matches = Vec::new(); + for (key, account) in &self.dashpay_external_accounts { + if let Some(m) = account.check_transaction_for_match(tx, Some(key.index)) { + matches.push(m); + } + } + matches + } } } @@ -559,6 +609,18 @@ impl ManagedAccount { } => AccountTypeMatch::ProviderPlatformKeys { involved_addresses: involved_other_addresses, }, + ManagedAccountType::DashpayReceivingFunds { + .. + } => AccountTypeMatch::DashpayReceivingFunds { + account_index: index.unwrap_or(0), + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::DashpayExternalAccount { + .. + } => AccountTypeMatch::DashpayExternalAccount { + account_index: index.unwrap_or(0), + 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 9a42e4325..19e9bd06c 100644 --- a/key-wallet/src/transaction_checking/transaction_router/mod.rs +++ b/key-wallet/src/transaction_checking/transaction_router/mod.rs @@ -72,7 +72,12 @@ impl TransactionRouter { pub fn get_relevant_account_types(tx_type: &TransactionType) -> Vec { match tx_type { TransactionType::Standard => { - vec![AccountTypeToCheck::StandardBIP44, AccountTypeToCheck::StandardBIP32] + vec![ + AccountTypeToCheck::StandardBIP44, + AccountTypeToCheck::StandardBIP32, + AccountTypeToCheck::DashpayReceivingFunds, + AccountTypeToCheck::DashpayExternalAccount, + ] } TransactionType::CoinJoin => vec![AccountTypeToCheck::CoinJoin], TransactionType::ProviderRegistration => vec![ @@ -171,6 +176,8 @@ pub enum AccountTypeToCheck { ProviderOwnerKeys, ProviderOperatorKeys, ProviderPlatformKeys, + DashpayReceivingFunds, + DashpayExternalAccount, } impl From for AccountTypeToCheck { @@ -214,6 +221,12 @@ impl From for AccountTypeToCheck { ManagedAccountType::ProviderPlatformKeys { .. } => AccountTypeToCheck::ProviderPlatformKeys, + ManagedAccountType::DashpayReceivingFunds { + .. + } => AccountTypeToCheck::DashpayReceivingFunds, + ManagedAccountType::DashpayExternalAccount { + .. + } => AccountTypeToCheck::DashpayExternalAccount, } } } @@ -259,6 +272,12 @@ impl From<&ManagedAccountType> for AccountTypeToCheck { ManagedAccountType::ProviderPlatformKeys { .. } => AccountTypeToCheck::ProviderPlatformKeys, + ManagedAccountType::DashpayReceivingFunds { + .. + } => AccountTypeToCheck::DashpayReceivingFunds, + ManagedAccountType::DashpayExternalAccount { + .. + } => AccountTypeToCheck::DashpayExternalAccount, } } } diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs b/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs index a98e19fb3..da27f1cfd 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs @@ -70,7 +70,7 @@ fn test_asset_unlock_classification() { #[test] fn test_asset_unlock_transaction_routing() { let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = @@ -134,7 +134,7 @@ fn test_asset_unlock_transaction_routing() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); // The transaction should be recognized as relevant assert!(result.is_relevant, "Asset unlock transaction should be recognized as relevant"); @@ -161,7 +161,7 @@ fn test_asset_unlock_routing_to_bip32_account() { let network = Network::Testnet; // Create wallet with default options (includes both BIP44 and BIP32) - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet"); let mut managed_wallet_info = @@ -214,7 +214,7 @@ fn test_asset_unlock_routing_to_bip32_account() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); // Should be recognized as relevant assert!(result.is_relevant, "Asset unlock transaction to BIP32 account should be relevant"); diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs index 60d6d012b..b667f684e 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs @@ -65,7 +65,7 @@ fn test_coinbase_transaction_routing_to_bip44_receive_address() { let network = Network::Testnet; // Create a wallet with a BIP44 account - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with BIP44 account for coinbase test"); let mut managed_wallet_info = @@ -113,7 +113,8 @@ fn test_coinbase_transaction_routing_to_bip44_receive_address() { &coinbase_tx, network, context, - Some(&wallet), // update state + &mut wallet, + true, // update state ); // The coinbase transaction should be recognized as relevant @@ -140,7 +141,7 @@ fn test_coinbase_transaction_routing_to_bip44_change_address() { let network = Network::Testnet; // Create a wallet with a BIP44 account - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with BIP44 account for coinbase change test"); let mut managed_wallet_info = @@ -188,7 +189,8 @@ fn test_coinbase_transaction_routing_to_bip44_change_address() { &coinbase_tx, network, context, - Some(&wallet), // update state + &mut wallet, + true, // update state ); // The coinbase transaction should be recognized as relevant even to change address @@ -214,7 +216,7 @@ fn test_coinbase_transaction_routing_to_bip44_change_address() { fn test_update_state_flag_behavior() { let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); @@ -255,9 +257,7 @@ fn test_update_state_flag_behavior() { }; // First check with update_state = false - let result1 = managed_wallet_info.check_transaction( - &tx, network, context, None, // don't update state - ); + let result1 = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, false); assert!(result1.is_relevant); @@ -282,7 +282,8 @@ fn test_update_state_flag_behavior() { &tx, network, context, - Some(&wallet), // update state + &mut wallet, + true, // update state ); assert!(result2.is_relevant); @@ -349,7 +350,7 @@ fn test_coinbase_routing() { fn test_coinbase_transaction_with_payload_routing() { // Test coinbase with special payload routing to BIP44 account let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet"); let mut managed_wallet_info = @@ -402,7 +403,7 @@ fn test_coinbase_transaction_with_payload_routing() { }; let result = - managed_wallet_info.check_transaction(&coinbase_tx, network, context, Some(&wallet)); + managed_wallet_info.check_transaction(&coinbase_tx, network, context, &mut wallet, true); assert!(result.is_relevant, "Coinbase with payload should be relevant"); assert_eq!(result.total_received, 5000000000, "Should have received block reward"); diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs b/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs index be1e50bbc..bd140095f 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs @@ -144,7 +144,7 @@ fn test_identity_registration_account_routing() { }; // First check without updating state - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); println!( "Identity registration transaction result: is_relevant={}, received={}, credit_conversion={}", @@ -178,7 +178,7 @@ fn test_identity_registration_account_routing() { fn test_normal_payment_to_identity_address_not_detected() { let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); @@ -223,7 +223,8 @@ fn test_normal_payment_to_identity_address_not_detected() { &normal_tx, network, context, - Some(&wallet), // update state + &mut wallet, + true, // update state ); // A normal transaction to an identity registration address should NOT be detected diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs index edf323ea4..2ebfbf600 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs @@ -128,7 +128,7 @@ fn test_provider_registration_transaction_routing_check_owner_only() { let other_wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut other_managed_wallet_info = @@ -229,7 +229,7 @@ fn test_provider_registration_transaction_routing_check_owner_only() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); println!( "Provider registration transaction result: is_relevant={}, received={}", @@ -264,7 +264,7 @@ fn test_provider_registration_transaction_routing_check_voting_only() { let other_wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut other_managed_wallet_info = @@ -365,7 +365,7 @@ fn test_provider_registration_transaction_routing_check_voting_only() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); println!( "Provider registration transaction result (voting): is_relevant={}, received={}", @@ -400,7 +400,7 @@ fn test_provider_registration_transaction_routing_check_operator_only() { let other_wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut other_managed_wallet_info = @@ -502,7 +502,7 @@ fn test_provider_registration_transaction_routing_check_operator_only() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); println!( "Provider registration transaction result (operator): is_relevant={}, received={}", @@ -582,7 +582,7 @@ fn test_provider_registration_transaction_routing_check_platform_only() { let other_wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut other_managed_wallet_info = @@ -706,7 +706,7 @@ fn test_provider_registration_transaction_routing_check_platform_only() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); println!( "Provider registration transaction result (platform): is_relevant={}, received={}", @@ -781,7 +781,7 @@ fn test_provider_update_service_with_operator_key() { fn test_provider_update_registrar_with_voting_and_operator() { // Test provider update registrar classification and routing let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = @@ -828,7 +828,7 @@ fn test_provider_update_registrar_with_voting_and_operator() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); // Should be recognized as relevant due to voting and operator keys assert!(result.is_relevant, "Provider update registrar should be relevant"); @@ -853,7 +853,7 @@ fn test_provider_update_registrar_with_voting_and_operator() { fn test_provider_revocation_classification_and_routing() { // Test that provider revocation transactions are properly classified and routed let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = @@ -922,7 +922,7 @@ fn test_provider_revocation_classification_and_routing() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); // Should be recognized as relevant due to collateral return assert!(result.is_relevant, "Provider revocation with collateral return should be relevant"); 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 a62696193..f54489e0a 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs @@ -40,7 +40,6 @@ fn test_standard_transaction_routing() { let tx_type = TransactionType::Standard; let accounts = TransactionRouter::get_relevant_account_types(&tx_type); - assert_eq!(accounts.len(), 2); assert!(accounts.contains(&AccountTypeToCheck::StandardBIP44)); assert!(accounts.contains(&AccountTypeToCheck::StandardBIP32)); } @@ -50,7 +49,7 @@ fn test_transaction_routing_to_bip44_account() { let network = Network::Testnet; // Create a wallet with a BIP44 account - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = @@ -96,7 +95,8 @@ fn test_transaction_routing_to_bip44_account() { &tx, network, context, - Some(&wallet), // update state + &mut wallet, + true, // update state ); // The transaction should be recognized as relevant since it sends to our address @@ -160,9 +160,7 @@ fn test_transaction_routing_to_bip32_account() { }; // Check with update_state = false - let result = managed_wallet_info.check_transaction( - &tx, network, context, None, // don't update state - ); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, false); // The transaction should be recognized as relevant assert!(result.is_relevant, "Transaction should be relevant to the BIP32 account"); @@ -184,7 +182,8 @@ fn test_transaction_routing_to_bip32_account() { &tx, network, context, - Some(&wallet), // update state + &mut wallet, + true, // update state ); assert!(result.is_relevant, "Transaction should still be relevant"); @@ -279,7 +278,7 @@ fn test_transaction_routing_to_coinjoin_account() { timestamp: Some(1234567890), }; - let result = managed_wallet_info.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, true); // This test may fail if CoinJoin detection is not properly implemented println!( @@ -385,7 +384,8 @@ fn test_transaction_affects_multiple_accounts() { &tx, network, context, - Some(&wallet), // update state + &mut wallet, + true, // update state ); // Transaction should be relevant and total should be sum of all outputs @@ -401,9 +401,7 @@ fn test_transaction_affects_multiple_accounts() { println!("Multi-account transaction result: accounts_affected={:?}", result.affected_accounts); // Test with update_state = false to ensure state isn't modified - let result2 = managed_wallet_info.check_transaction( - &tx, network, context, None, // don't update state - ); + let result2 = managed_wallet_info.check_transaction(&tx, network, context, &mut wallet, false); assert_eq!( result2.total_received, result.total_received, diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/standard_transactions.rs b/key-wallet/src/transaction_checking/transaction_router/tests/standard_transactions.rs index 1515fc1b8..11de12b8f 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/standard_transactions.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/standard_transactions.rs @@ -20,7 +20,6 @@ fn test_single_input_two_outputs_payment() { assert_eq!(tx_type, TransactionType::Standard); let accounts = TransactionRouter::get_relevant_account_types(&tx_type); - assert_eq!(accounts.len(), 2); assert!(accounts.contains(&AccountTypeToCheck::StandardBIP44)); assert!(accounts.contains(&AccountTypeToCheck::StandardBIP32)); } @@ -83,7 +82,6 @@ fn test_payment_to_multiple_recipients() { assert_eq!(tx_type, TransactionType::Standard); let accounts = TransactionRouter::get_relevant_account_types(&tx_type); - assert_eq!(accounts.len(), 2); assert!(accounts.contains(&AccountTypeToCheck::StandardBIP44)); assert!(accounts.contains(&AccountTypeToCheck::StandardBIP32)); } diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 86717cf7f..15f47ff25 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -37,8 +37,13 @@ pub enum TransactionContext { pub trait WalletTransactionChecker { /// Check if a transaction belongs to this wallet with optimized routing /// Only checks relevant account types based on transaction type - /// If update_state_if_found is Some, updates account state when transaction is found. - /// The wallet is needed to generate more addresses. + /// + /// The mutable wallet reference is required to support address generation and potential + /// platform queries (e.g., for DashPay transactions). + /// + /// If `update_state` is true, updates account state (transactions, UTXOs, balances, addresses). + /// If `update_state` is false, only checks relevance without modifying state (useful for previews). + /// /// The context parameter indicates where the transaction comes from (mempool, block, etc.) /// fn check_transaction( @@ -46,7 +51,8 @@ pub trait WalletTransactionChecker { tx: &Transaction, network: Network, context: TransactionContext, - update_state_with_wallet_if_found: Option<&Wallet>, + wallet: &mut Wallet, + update_state: bool, ) -> TransactionCheckResult; } @@ -56,7 +62,8 @@ impl WalletTransactionChecker for ManagedWalletInfo { tx: &Transaction, network: Network, context: TransactionContext, - update_state_with_wallet_if_found: Option<&Wallet>, + wallet: &mut Wallet, + update_state: bool, ) -> TransactionCheckResult { // Get the account collection for this network if let Some(collection) = self.accounts.get(&network) { @@ -70,7 +77,16 @@ impl WalletTransactionChecker for ManagedWalletInfo { let result = collection.check_transaction(tx, &relevant_types); // Update state if requested and transaction is relevant - if update_state_with_wallet_if_found.is_some() && result.is_relevant { + if update_state && result.is_relevant { + // Check if this is an immature coinbase transaction before processing accounts + let is_coinbase = tx.is_coin_base(); + let needs_maturity = is_coinbase + && matches!( + context, + TransactionContext::InBlock { .. } + | TransactionContext::InChainLockedBlock { .. } + ); + if let Some(collection) = self.accounts.get_mut(&network) { for account_match in &result.affected_accounts { // Find and update the specific account @@ -113,6 +129,15 @@ impl WalletTransactionChecker for ManagedWalletInfo { AccountTypeMatch::ProviderPlatformKeys { .. } => collection.provider_platform_keys.as_mut(), + AccountTypeMatch::DashpayReceivingFunds { + .. + } + | AccountTypeMatch::DashpayExternalAccount { + .. + } => { + // DashPay managed accounts are not persisted here yet + None + } }; if let Some(account) = account { @@ -147,53 +172,21 @@ impl WalletTransactionChecker for ManagedWalletInfo { is_ours: net_amount < 0, }; - // Check if this is an immature transaction (coinbase that needs maturity) - let is_coinbase = tx.is_coin_base(); - let needs_maturity = is_coinbase - && matches!( - context, - TransactionContext::InBlock { .. } - | TransactionContext::InChainLockedBlock { .. } - ); - - if needs_maturity { - // Handle as immature transaction - if let TransactionContext::InBlock { - height, - block_hash, - timestamp, - } - | TransactionContext::InChainLockedBlock { - height, - block_hash, - timestamp, - } = context - { - // Create immature transaction - let _immature_tx = ImmatureTransaction::new( - tx.clone(), - height, - block_hash.unwrap_or_else(BlockHash::all_zeros), - timestamp.unwrap_or(0) as u64, - 100, // Standard coinbase maturity - true, // is_coinbase - ); - - // todo!() - // Track in immature transactions instead of regular transactions - // This would need to be implemented in the account - // For now, we'll still add to regular transactions - } + // For immature transactions, skip adding to regular transactions + // They will be added when they mature via process_matured_transactions + if !needs_maturity { + account.transactions.insert(tx.txid(), tx_record); } - account.transactions.insert(tx.txid(), tx_record); - // Ingest UTXOs for outputs that pay to our addresses and // remove UTXOs that are spent by this transaction's inputs. // Only apply for spendable account types (Standard, CoinJoin). + // Skip UTXO creation for immature coinbase transactions. match &mut account.account_type { crate::managed_account::managed_account_type::ManagedAccountType::Standard { .. } - | crate::managed_account::managed_account_type::ManagedAccountType::CoinJoin { .. } => { + | crate::managed_account::managed_account_type::ManagedAccountType::CoinJoin { .. } + | crate::managed_account::managed_account_type::ManagedAccountType::DashpayReceivingFunds { .. } + | crate::managed_account::managed_account_type::ManagedAccountType::DashpayExternalAccount { .. } => { // Build a set of addresses involved for fast membership tests let mut involved_addrs = alloc::collections::BTreeSet::new(); for info in account_match.account_type_match.all_involved_addresses() { @@ -207,26 +200,28 @@ impl WalletTransactionChecker for ManagedWalletInfo { | TransactionContext::InChainLockedBlock { height, .. } => (true, height), }; - // Insert UTXOs for matching outputs - let txid = tx.txid(); - for (vout, output) in tx.output.iter().enumerate() { - if let Ok(addr) = DashAddress::from_script(&output.script_pubkey, network) { - if involved_addrs.contains(&addr) { - let outpoint = OutPoint { txid, vout: vout as u32 }; - // Construct TxOut clone explicitly to avoid trait assumptions - let txout = dashcore::TxOut { - value: output.value, - script_pubkey: output.script_pubkey.clone(), - }; - let mut utxo = Utxo::new( - outpoint, - txout, - addr, - utxo_height, - tx.is_coin_base(), - ); - utxo.is_confirmed = is_confirmed; - account.utxos.insert(outpoint, utxo); + // Insert UTXOs for matching outputs (skip for immature coinbase) + if !needs_maturity { + let txid = tx.txid(); + for (vout, output) in tx.output.iter().enumerate() { + if let Ok(addr) = DashAddress::from_script(&output.script_pubkey, network) { + if involved_addrs.contains(&addr) { + let outpoint = OutPoint { txid, vout: vout as u32 }; + // Construct TxOut clone explicitly to avoid trait assumptions + let txout = dashcore::TxOut { + value: output.value, + script_pubkey: output.script_pubkey.clone(), + }; + let mut utxo = Utxo::new( + outpoint, + txout, + addr, + utxo_height, + tx.is_coin_base(), + ); + utxo.is_confirmed = is_confirmed; + account.utxos.insert(outpoint, utxo); + } } } } @@ -268,42 +263,105 @@ impl WalletTransactionChecker for ManagedWalletInfo { account.mark_address_used(&address_info.address); } - // Generate new addresses up to the gap limit if wallet is provided - if let Some(wallet) = update_state_with_wallet_if_found { - // Get the account's xpub from the wallet for address generation - let account_type_to_check = - account_match.account_type_match.to_account_type_to_check(); - let xpub_opt = wallet.extended_public_key_for_account_type( - &account_type_to_check, - account_match.account_type_match.account_index(), - network, - ); - - // Maintain gap limit for the address pools - if let Some(xpub) = xpub_opt { - let key_source = - crate::managed_account::address_pool::KeySource::Public( - xpub, - ); - - // For standard accounts, maintain gap limit on both pools - if let crate::managed_account::managed_account_type::ManagedAccountType::Standard { - external_addresses, - internal_addresses, + // Generate new addresses up to the gap limit + // Get the account's xpub from the wallet for address generation + let account_type_to_check = + account_match.account_type_match.to_account_type_to_check(); + let xpub_opt = wallet.extended_public_key_for_account_type( + &account_type_to_check, + account_match.account_type_match.account_index(), + network, + ); + + // Maintain gap limit for the address pools + if let Some(xpub) = xpub_opt { + let key_source = + crate::managed_account::address_pool::KeySource::Public(xpub); + + // For standard accounts, maintain gap limit on both pools + if let crate::managed_account::managed_account_type::ManagedAccountType::Standard { + external_addresses, + internal_addresses, + .. + } = &mut account.account_type { + // Maintain gap limit for external addresses + let _ = external_addresses.maintain_gap_limit(&key_source); + // Maintain gap limit for internal addresses + let _ = internal_addresses.maintain_gap_limit(&key_source); + } else { + // For other account types, get the single address pool + for pool in account.account_type.address_pools_mut() { + let _ = pool.maintain_gap_limit(&key_source); + } + } + } + } + } + + // Store immature transaction if this is a coinbase in a block + if needs_maturity { + if let TransactionContext::InBlock { + height, + block_hash, + timestamp, + } + | TransactionContext::InChainLockedBlock { + height, + block_hash, + timestamp, + } = context + { + // Create immature transaction + let mut immature_tx = ImmatureTransaction::new( + tx.clone(), + height, + block_hash.unwrap_or_else(BlockHash::all_zeros), + timestamp.unwrap_or(0) as u64, + 100, // Standard coinbase maturity (100 blocks) + true, // is_coinbase + ); + + // Populate affected accounts from result + use super::account_checker::AccountTypeMatch; + for account_match in &result.affected_accounts { + match &account_match.account_type_match { + AccountTypeMatch::StandardBIP44 { + account_index, .. - } = &mut account.account_type { - // Maintain gap limit for external addresses - let _ = external_addresses.maintain_gap_limit(&key_source); - // Maintain gap limit for internal addresses - let _ = internal_addresses.maintain_gap_limit(&key_source); - } else { - // For other account types, get the single address pool - for pool in account.account_type.address_pools_mut() { - let _ = pool.maintain_gap_limit(&key_source); - } + } => { + immature_tx.affected_accounts.add_bip44(*account_index); + } + AccountTypeMatch::StandardBIP32 { + account_index, + .. + } => { + immature_tx.affected_accounts.add_bip32(*account_index); + } + AccountTypeMatch::CoinJoin { + account_index, + .. + } => { + immature_tx.affected_accounts.add_coinjoin(*account_index); + } + _ => { + // Other account types don't track immature transactions } } } + + // Set total received amount + immature_tx.total_received = result.total_received; + + // Store in wallet's immature transaction collection + self.add_immature_transaction(network, immature_tx); + + tracing::info!( + txid = %tx.txid(), + height = height, + maturity_height = height + 100, + received = result.total_received, + "Coinbase transaction stored as immature" + ); } } @@ -410,7 +468,10 @@ mod tests { let context = TransactionContext::Mempool; // Check transaction on different network (should have no accounts) - let result = managed_wallet.check_transaction(&tx, other_network, context, None); + // Note: Even though we don't have accounts on this network, we still need to pass wallet + let mut wallet_mut = wallet; + let result = + managed_wallet.check_transaction(&tx, other_network, context, &mut wallet_mut, true); // Should return default result with no relevance assert!(!result.is_relevant); @@ -463,63 +524,82 @@ mod tests { let mut managed_wallet = ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - // Get addresses from different accounts to trigger different branches - let account_collection = wallet.accounts.get(&network).expect("Should have accounts"); - - // Get BIP32 account address - if let Some(bip32_account) = account_collection.standard_bip32_accounts.get(&0) { - let xpub = bip32_account.account_xpub; - if let Some(managed_account) = managed_wallet.first_bip32_managed_account_mut(network) { - let address = managed_account - .next_receive_address(Some(&xpub), true) - .expect("Should get BIP32 address"); - - let tx = create_transaction_to_address(&address, 50_000); - - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Should create block hash"), - ), - timestamp: Some(1234567890), - }; - - // This should exercise BIP32 account branch in the update logic - let result = managed_wallet.check_transaction(&tx, network, context, Some(&wallet)); - - // Should be relevant since it's our address - assert!(result.is_relevant); - assert_eq!(result.total_received, 50_000); + // Get BIP32 account address - scope the immutable borrow + let (bip32_xpub, bip32_address) = { + let account_collection = wallet.accounts.get(&network).expect("Should have accounts"); + if let Some(bip32_account) = account_collection.standard_bip32_accounts.get(&0) { + let xpub = bip32_account.account_xpub; + if let Some(managed_account) = + managed_wallet.first_bip32_managed_account_mut(network) + { + let address = managed_account + .next_receive_address(Some(&xpub), true) + .expect("Should get BIP32 address"); + (Some(xpub), Some(address)) + } else { + (None, None) + } + } else { + (None, None) } + }; + + if let (Some(_xpub), Some(address)) = (bip32_xpub, bip32_address) { + let tx = create_transaction_to_address(&address, 50_000); + + let context = TransactionContext::InBlock { + height: 100000, + block_hash: Some( + BlockHash::from_slice(&[0u8; 32]).expect("Should create block hash"), + ), + timestamp: Some(1234567890), + }; + + // This should exercise BIP32 account branch in the update logic + let result = managed_wallet.check_transaction(&tx, network, context, &mut wallet, true); + + // Should be relevant since it's our address + assert!(result.is_relevant); + assert_eq!(result.total_received, 50_000); } - // Get CoinJoin account address - if let Some(coinjoin_account) = account_collection.coinjoin_accounts.get(&0) { - let xpub = coinjoin_account.account_xpub; - if let Some(managed_account) = - managed_wallet.first_coinjoin_managed_account_mut(network) - { - let address = managed_account - .next_address(Some(&xpub), true) - .expect("Should get CoinJoin address"); - - let tx = create_transaction_to_address(&address, 75_000); - - let context = TransactionContext::InChainLockedBlock { - height: 100001, - block_hash: Some( - BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash"), - ), - timestamp: Some(1234567891), - }; - - // This should exercise CoinJoin account branch in the update logic - let result = managed_wallet.check_transaction(&tx, network, context, Some(&wallet)); - - // Since this is not a coinjoin looking transaction, we should not pick up on it. - assert!(!result.is_relevant); - assert_eq!(result.total_received, 0); + // Get CoinJoin account address - scope the immutable borrow + let (coinjoin_xpub, coinjoin_address) = { + let account_collection = wallet.accounts.get(&network).expect("Should have accounts"); + if let Some(coinjoin_account) = account_collection.coinjoin_accounts.get(&0) { + let xpub = coinjoin_account.account_xpub; + if let Some(managed_account) = + managed_wallet.first_coinjoin_managed_account_mut(network) + { + let address = managed_account + .next_address(Some(&xpub), true) + .expect("Should get CoinJoin address"); + (Some(xpub), Some(address)) + } else { + (None, None) + } + } else { + (None, None) } + }; + + if let (Some(_xpub), Some(address)) = (coinjoin_xpub, coinjoin_address) { + let tx = create_transaction_to_address(&address, 75_000); + + let context = TransactionContext::InChainLockedBlock { + height: 100001, + block_hash: Some( + BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash"), + ), + timestamp: Some(1234567891), + }; + + // This should exercise CoinJoin account branch in the update logic + let result = managed_wallet.check_transaction(&tx, network, context, &mut wallet, true); + + // Since this is not a coinjoin looking transaction, we should not pick up on it. + assert!(!result.is_relevant); + assert_eq!(result.total_received, 0); } } @@ -527,7 +607,7 @@ mod tests { #[test] fn test_wallet_checker_coinbase_immature_handling() { let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Should create wallet"); let mut managed_wallet = @@ -573,25 +653,37 @@ mod tests { }; let result = - managed_wallet.check_transaction(&coinbase_tx, network, context, Some(&wallet)); + managed_wallet.check_transaction(&coinbase_tx, network, context, &mut wallet, true); // Should be relevant assert!(result.is_relevant); assert_eq!(result.total_received, 5_000_000_000); - // The transaction should be stored even though it's immature + // The transaction should be stored in immature collection, not regular transactions let managed_account = managed_wallet .first_bip44_managed_account(network) .expect("Should have managed account"); - assert!(managed_account.transactions.contains_key(&coinbase_tx.txid())); + // Should NOT be in regular transactions yet + assert!( + !managed_account.transactions.contains_key(&coinbase_tx.txid()), + "Immature coinbase should not be in regular transactions" + ); + + // Should be in immature collection + let immature_txs = + managed_wallet.immature_transactions(network).expect("Should have immature collection"); + assert!( + immature_txs.contains(&coinbase_tx.txid()), + "Coinbase should be in immature collection" + ); } /// Test that spending a wallet-owned UTXO without creating change is detected #[test] fn test_wallet_checker_detects_spend_only_transaction() { let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Should create wallet"); let mut managed_wallet = @@ -617,8 +709,13 @@ mod tests { timestamp: Some(1_650_000_000), }; - let funding_result = - managed_wallet.check_transaction(&funding_tx, network, funding_context, Some(&wallet)); + let funding_result = managed_wallet.check_transaction( + &funding_tx, + network, + funding_context, + &mut wallet, + true, + ); assert!(funding_result.is_relevant, "Funding transaction must be relevant"); assert_eq!(funding_result.total_received, funding_value); @@ -653,7 +750,7 @@ mod tests { }; let spend_result = - managed_wallet.check_transaction(&spend_tx, network, spend_context, Some(&wallet)); + managed_wallet.check_transaction(&spend_tx, network, spend_context, &mut wallet, true); assert!(spend_result.is_relevant, "Spend transaction should be detected"); assert_eq!(spend_result.total_received, 0); @@ -677,11 +774,138 @@ mod tests { assert_eq!(record.net_amount, -(funding_value as i64)); } + /// Test that immature coinbase transactions are properly stored and processed + #[test] + fn test_wallet_checker_immature_transaction_flow() { + use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; + + let network = Network::Testnet; + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + .expect("Should create wallet"); + + let mut managed_wallet = + ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); + + // Get a wallet address + let account_collection = wallet.accounts.get(&network).expect("Should have accounts"); + let account = + account_collection.standard_bip44_accounts.get(&0).expect("Should have BIP44 account"); + let xpub = account.account_xpub; + + let address = managed_wallet + .first_bip44_managed_account_mut(network) + .expect("Should have managed account") + .next_receive_address(Some(&xpub), true) + .expect("Should get address"); + + // Create a coinbase transaction + let coinbase_tx = Transaction { + version: 2, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::all_zeros(), // Coinbase has null previous output + vout: 0xffffffff, + }, + script_sig: ScriptBuf::new(), + sequence: 0xffffffff, + witness: dashcore::Witness::new(), + }], + output: vec![TxOut { + value: 5_000_000_000, // 50 DASH block reward + script_pubkey: address.script_pubkey(), + }], + special_transaction_payload: None, + }; + + let block_height = 100000; + let context = TransactionContext::InBlock { + height: block_height, + block_hash: Some(BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash")), + timestamp: Some(1234567890), + }; + + // Process the coinbase transaction + let result = + managed_wallet.check_transaction(&coinbase_tx, network, context, &mut wallet, true); + + // Should be relevant + assert!(result.is_relevant); + assert_eq!(result.total_received, 5_000_000_000); + + // Verify transaction is NOT in regular transactions yet + let managed_account = managed_wallet + .first_bip44_managed_account(network) + .expect("Should have managed account"); + assert!( + !managed_account.transactions.contains_key(&coinbase_tx.txid()), + "Immature coinbase should not be in regular transactions" + ); + + // Verify transaction IS in immature collection + let immature_txs = + managed_wallet.immature_transactions(network).expect("Should have immature collection"); + assert!( + immature_txs.contains(&coinbase_tx.txid()), + "Coinbase should be in immature collection" + ); + + // Verify the immature transaction has correct data + let immature_tx = immature_txs.get(&coinbase_tx.txid()).expect("Should have immature tx"); + assert_eq!(immature_tx.height, block_height); + assert_eq!(immature_tx.total_received, 5_000_000_000); + assert_eq!(immature_tx.maturity_confirmations, 100); + assert!(immature_tx.is_coinbase); + assert!(immature_tx.affected_accounts.bip44_accounts.contains(&0)); + + // Verify no UTXOs were created (since it's immature) + assert!(managed_account.utxos.is_empty(), "No UTXOs should exist for immature coinbase"); + + // Verify balance is still zero + assert_eq!( + managed_wallet.balance().total, + 0, + "Balance should be zero while coinbase is immature" + ); + + // Verify immature balance is tracked + let immature_balance = managed_wallet.network_immature_balance(network); + assert_eq!( + immature_balance, 5_000_000_000, + "Immature balance should reflect the coinbase value" + ); + + // Now advance the chain height past maturity (100 blocks) + let mature_height = block_height + 100; + managed_wallet.update_chain_height(network, mature_height); + + // Verify transaction moved from immature to regular + let managed_account = managed_wallet + .first_bip44_managed_account(network) + .expect("Should have managed account"); + assert!( + managed_account.transactions.contains_key(&coinbase_tx.txid()), + "Matured coinbase should be in regular transactions" + ); + + // Verify transaction is no longer immature + let immature_txs = + managed_wallet.immature_transactions(network).expect("Should have immature collection"); + assert!( + !immature_txs.contains(&coinbase_tx.txid()), + "Matured coinbase should not be in immature collection" + ); + + // Verify immature balance is now zero + let immature_balance = managed_wallet.network_immature_balance(network); + assert_eq!(immature_balance, 0, "Immature balance should be zero after maturity"); + } + /// Test mempool context for timestamp/height handling #[test] fn test_wallet_checker_mempool_context() { let network = Network::Testnet; - let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Should create wallet"); let mut managed_wallet = @@ -704,7 +928,7 @@ mod tests { // Test with Mempool context let context = TransactionContext::Mempool; - let result = managed_wallet.check_transaction(&tx, network, context, Some(&wallet)); + let result = managed_wallet.check_transaction(&tx, network, context, &mut wallet, true); // Should be relevant assert!(result.is_relevant); diff --git a/key-wallet/src/wallet/helper.rs b/key-wallet/src/wallet/helper.rs index 6b3da1c8e..a9ac07be3 100644 --- a/key-wallet/src/wallet/helper.rs +++ b/key-wallet/src/wallet/helper.rs @@ -868,6 +868,11 @@ impl Wallet { // These use BLS/EdDSA keys, not regular xpubs None } + crate::transaction_checking::transaction_router::AccountTypeToCheck::DashpayReceivingFunds | + crate::transaction_checking::transaction_router::AccountTypeToCheck::DashpayExternalAccount => { + // Currently not retrieved via this helper + None + } } }) } diff --git a/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs index b862e9290..4d79a2462 100644 --- a/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs +++ b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs @@ -107,6 +107,10 @@ pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccount fee_level: FeeLevel, current_block_height: u32, ) -> Result; + + /// Update chain state and process any matured transactions + /// This should be called when the chain tip advances to a new height + fn update_chain_height(&mut self, network: Network, current_height: u32); } /// Default implementation for ManagedWalletInfo @@ -351,4 +355,18 @@ impl WalletInfoInterface for ManagedWalletInfo { current_block_height, ) } + + fn update_chain_height(&mut self, network: Network, current_height: u32) { + // Process any matured transactions for this network + let matured = self.process_matured_transactions(network, current_height); + + if !matured.is_empty() { + tracing::info!( + network = ?network, + current_height = current_height, + matured_count = matured.len(), + "Processed matured coinbase transactions" + ); + } + } }