From 1b7d7305276d4adf63f32d706edb90b01ddc7a2e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 5 Mar 2026 14:20:58 +0700 Subject: [PATCH 1/3] feat(platform-wallet): add gap-limit identity discovery scan Add DashSync-style identity discovery to PlatformWalletInfo that scans consecutive DIP-13 authentication key indices and queries Platform to find registered identities during wallet sync. New methods: - discover_identities: gap-limit scan using PublicKeyHash queries - discover_identities_with_contacts: same + fetches DashPay contacts Refactors shared key derivation and contact request parsing into reusable modules used by both discovery and asset lock processing. Co-Authored-By: Claude Opus 4.6 --- .../identity_discovery.rs | 189 ++++++++++++++++++ .../platform_wallet_info/key_derivation.rs | 76 +++++++ .../matured_transactions.rs | 153 +------------- .../src/platform_wallet_info/mod.rs | 94 +++++++++ 4 files changed, 365 insertions(+), 147 deletions(-) create mode 100644 packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs create mode 100644 packages/rs-platform-wallet/src/platform_wallet_info/key_derivation.rs diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs new file mode 100644 index 00000000000..fafd2d89b82 --- /dev/null +++ b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs @@ -0,0 +1,189 @@ +//! Gap-limit identity discovery for wallet sync +//! +//! This module implements DashSync-style gap-limit scanning for identities +//! during wallet sync. It derives consecutive authentication keys from the +//! wallet's BIP32 tree and queries Platform to find registered identities. + +use super::key_derivation::derive_identity_auth_key_hash; +use super::parse_contact_request_document; +use super::PlatformWalletInfo; +use crate::error::PlatformWalletError; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::prelude::Identifier; +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; + +impl PlatformWalletInfo { + /// Discover identities by scanning consecutive identity indices with a gap limit + /// + /// Starting from `start_index`, derives ECDSA authentication keys for consecutive + /// identity indices and queries Platform for registered identities. Scanning stops + /// when `gap_limit` consecutive indices yield no identity. + /// + /// This mirrors the DashSync gap-limit approach: keep scanning until N consecutive + /// misses, then stop. + /// + /// # Arguments + /// + /// * `wallet` - The wallet to derive authentication keys from + /// * `start_index` - The first identity index to check + /// * `gap_limit` - Number of consecutive misses before stopping (typically 5) + /// + /// # Returns + /// + /// Returns the list of newly discovered identity IDs + pub async fn discover_identities( + &mut self, + wallet: &key_wallet::Wallet, + start_index: u32, + gap_limit: u32, + ) -> Result, PlatformWalletError> { + use dash_sdk::platform::types::identity::PublicKeyHash; + use dash_sdk::platform::Fetch; + + let sdk = self + .identity_manager() + .sdk + .as_ref() + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "SDK not configured in identity manager".to_string(), + ) + })? + .clone(); + + let network = self.network(); + let mut discovered = Vec::new(); + let mut consecutive_misses = 0u32; + let mut identity_index = start_index; + + while consecutive_misses < gap_limit { + // Derive the authentication key hash for this identity index (key_index 0) + let key_hash_array = + derive_identity_auth_key_hash(wallet, network, identity_index, 0)?; + + // Query Platform for an identity registered with this key hash + match dpp::identity::Identity::fetch(&sdk, PublicKeyHash(key_hash_array)).await { + Ok(Some(identity)) => { + let identity_id = identity.id(); + + // Add to manager if not already present + if !self + .identity_manager() + .identities() + .contains_key(&identity_id) + { + self.identity_manager_mut().add_identity(identity)?; + } + + discovered.push(identity_id); + consecutive_misses = 0; + } + Ok(None) => { + consecutive_misses += 1; + } + Err(e) => { + eprintln!( + "Failed to query identity by public key hash at index {}: {}", + identity_index, e + ); + consecutive_misses += 1; + } + } + + identity_index += 1; + } + + Ok(discovered) + } + + /// Discover identities and fetch their DashPay contact requests + /// + /// Calls [`discover_identities`] then fetches sent and received contact requests + /// for each discovered identity, storing them in the identity manager. + /// + /// # Arguments + /// + /// * `wallet` - The wallet to derive authentication keys from + /// * `start_index` - The first identity index to check + /// * `gap_limit` - Number of consecutive misses before stopping (typically 5) + /// + /// # Returns + /// + /// Returns the list of newly discovered identity IDs + pub async fn discover_identities_with_contacts( + &mut self, + wallet: &key_wallet::Wallet, + start_index: u32, + gap_limit: u32, + ) -> Result, PlatformWalletError> { + let discovered = self + .discover_identities(wallet, start_index, gap_limit) + .await?; + + if discovered.is_empty() { + return Ok(discovered); + } + + let sdk = self + .identity_manager() + .sdk + .as_ref() + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "SDK not configured in identity manager".to_string(), + ) + })? + .clone(); + + for identity_id in &discovered { + // Get the identity from the manager to pass to the SDK + let identity = match self.identity_manager().identity(identity_id) { + Some(id) => id.clone(), + None => continue, + }; + + match sdk + .fetch_all_contact_requests_for_identity(&identity, Some(100)) + .await + { + Ok((sent_docs, received_docs)) => { + // Process sent contact requests + for (_doc_id, maybe_doc) in sent_docs { + if let Some(doc) = maybe_doc { + if let Ok(contact_request) = parse_contact_request_document(&doc) { + if let Some(managed_identity) = self + .identity_manager_mut() + .managed_identity_mut(identity_id) + { + managed_identity.add_sent_contact_request(contact_request); + } + } + } + } + + // Process received contact requests + for (_doc_id, maybe_doc) in received_docs { + if let Some(doc) = maybe_doc { + if let Ok(contact_request) = parse_contact_request_document(&doc) { + if let Some(managed_identity) = self + .identity_manager_mut() + .managed_identity_mut(identity_id) + { + managed_identity.add_incoming_contact_request(contact_request); + } + } + } + } + } + Err(e) => { + eprintln!( + "Failed to fetch contact requests for identity {}: {}", + identity_id, e + ); + } + } + } + + Ok(discovered) + } +} diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/key_derivation.rs b/packages/rs-platform-wallet/src/platform_wallet_info/key_derivation.rs new file mode 100644 index 00000000000..87b1e63a5b2 --- /dev/null +++ b/packages/rs-platform-wallet/src/platform_wallet_info/key_derivation.rs @@ -0,0 +1,76 @@ +//! Shared key derivation utilities for identity authentication keys +//! +//! This module provides helper functions used by both the matured transactions +//! processor and the identity discovery scanner. + +use crate::error::PlatformWalletError; +use key_wallet::Network; + +/// Derive the 20-byte RIPEMD160(SHA256) hash of the public key at the given +/// identity authentication path. +/// +/// Path format: `base_path / identity_index' / key_index'` +/// where `base_path` is `m/9'/COIN_TYPE'/5'/0'` (mainnet or testnet). +/// +/// # Arguments +/// +/// * `wallet` - The wallet to derive keys from +/// * `network` - Network to select the correct coin type +/// * `identity_index` - The identity index (hardened) +/// * `key_index` - The key index within that identity (hardened) +/// +/// # Returns +/// +/// Returns the 20-byte public key hash suitable for Platform identity lookup. +pub(crate) fn derive_identity_auth_key_hash( + wallet: &key_wallet::Wallet, + network: Network, + identity_index: u32, + key_index: u32, +) -> Result<[u8; 20], PlatformWalletError> { + use dashcore::secp256k1::Secp256k1; + use dpp::util::hash::ripemd160_sha256; + use key_wallet::bip32::{ChildNumber, DerivationPath, ExtendedPubKey}; + use key_wallet::dip9::{ + IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET, + }; + + let base_path = match network { + Network::Dash => IDENTITY_AUTHENTICATION_PATH_MAINNET, + Network::Testnet => IDENTITY_AUTHENTICATION_PATH_TESTNET, + _ => { + return Err(PlatformWalletError::InvalidIdentityData( + "Unsupported network for identity derivation".to_string(), + )); + } + }; + + let mut full_path = DerivationPath::from(base_path); + full_path = full_path.extend([ + ChildNumber::from_hardened_idx(identity_index).map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!("Invalid identity index: {}", e)) + })?, + ChildNumber::from_hardened_idx(key_index).map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!("Invalid key index: {}", e)) + })?, + ]); + + let auth_key = wallet + .derive_extended_private_key(&full_path) + .map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!( + "Failed to derive authentication key: {}", + e + )) + })?; + + let secp = Secp256k1::new(); + let public_key = ExtendedPubKey::from_priv(&secp, &auth_key); + let public_key_bytes = public_key.public_key.serialize(); + let key_hash = ripemd160_sha256(&public_key_bytes); + + let mut key_hash_array = [0u8; 20]; + key_hash_array.copy_from_slice(&key_hash); + + Ok(key_hash_array) +} diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs b/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs index b5a3ceec052..4c23665b3e1 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs @@ -3,15 +3,13 @@ //! This module handles the detection and fetching of identities created from //! asset lock transactions. +use super::key_derivation::derive_identity_auth_key_hash; +use super::parse_contact_request_document; use super::PlatformWalletInfo; use crate::error::PlatformWalletError; -#[allow(unused_imports)] -use crate::ContactRequest; +use dpp::identity::accessors::IdentityGettersV0; use dpp::prelude::Identifier; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; -use key_wallet::Network; - -use dpp::identity::accessors::IdentityGettersV0; impl PlatformWalletInfo { /// Discover identity and fetch contact requests for a single asset lock transaction @@ -62,7 +60,6 @@ impl PlatformWalletInfo { ) -> Result, PlatformWalletError> { use dash_sdk::platform::types::identity::PublicKeyHash; use dash_sdk::platform::Fetch; - use dpp::util::hash::ripemd160_sha256; let mut identities_processed = Vec::new(); @@ -83,59 +80,9 @@ impl PlatformWalletInfo { })? .clone(); - // Derive the first authentication key (identity_index 0, key_index 0) - let identity_index = 0u32; - let key_index = 0u32; - - // Build identity authentication derivation path - // Path format: m/9'/COIN_TYPE'/5'/0'/identity_index'/key_index' - use key_wallet::bip32::{ChildNumber, DerivationPath}; - use key_wallet::dip9::{ - IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET, - }; - - let base_path = match self.network() { - Network::Dash => IDENTITY_AUTHENTICATION_PATH_MAINNET, - Network::Testnet => IDENTITY_AUTHENTICATION_PATH_TESTNET, - _ => { - return Err(PlatformWalletError::InvalidIdentityData( - "Unsupported network for identity derivation".to_string(), - )); - } - }; - - // Create full derivation path: base path + identity_index' + key_index' - let mut full_path = DerivationPath::from(base_path); - full_path = full_path.extend([ - ChildNumber::from_hardened_idx(identity_index).map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!("Invalid identity index: {}", e)) - })?, - ChildNumber::from_hardened_idx(key_index).map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!("Invalid key index: {}", e)) - })?, - ]); - - // Derive the extended private key at this path - let auth_key = wallet - .derive_extended_private_key(&full_path) - .map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!( - "Failed to derive authentication key: {}", - e - )) - })?; - - // Get public key bytes and hash them - use dashcore::secp256k1::Secp256k1; - use key_wallet::bip32::ExtendedPubKey; - let secp = Secp256k1::new(); - let public_key = ExtendedPubKey::from_priv(&secp, &auth_key); - let public_key_bytes = public_key.public_key.serialize(); - let key_hash = ripemd160_sha256(&public_key_bytes); - - // Create a fixed-size array from the hash - let mut key_hash_array = [0u8; 20]; - key_hash_array.copy_from_slice(&key_hash); + // Derive the first authentication key hash (identity_index 0, key_index 0) + let key_hash_array = + derive_identity_auth_key_hash(wallet, self.network(), 0, 0)?; // Query Platform for identity by public key hash match dpp::identity::Identity::fetch(&sdk, PublicKeyHash(key_hash_array)).await { @@ -209,91 +156,3 @@ impl PlatformWalletInfo { Ok(identities_processed) } } - -/// Parse a contact request document into a ContactRequest struct -fn parse_contact_request_document( - doc: &dpp::document::Document, -) -> Result { - use dpp::document::DocumentV0Getters; - use dpp::platform_value::Value; - - // Extract fields from the document - let properties = doc.properties(); - - let to_user_id = properties - .get("toUserId") - .and_then(|v| match v { - Value::Identifier(id) => Some(Identifier::from(*id)), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid toUserId in contact request".to_string(), - ) - })?; - - let sender_key_index = properties - .get("senderKeyIndex") - .and_then(|v| match v { - Value::U32(i) => Some(*i), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid senderKeyIndex in contact request".to_string(), - ) - })?; - - let recipient_key_index = properties - .get("recipientKeyIndex") - .and_then(|v| match v { - Value::U32(i) => Some(*i), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid recipientKeyIndex in contact request".to_string(), - ) - })?; - - let account_reference = properties - .get("accountReference") - .and_then(|v| match v { - Value::U32(i) => Some(*i), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid accountReference in contact request".to_string(), - ) - })?; - - let encrypted_public_key = properties - .get("encryptedPublicKey") - .and_then(|v| match v { - Value::Bytes(b) => Some(b.clone()), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid encryptedPublicKey in contact request".to_string(), - ) - })?; - - let created_at_core_block_height = doc.created_at_core_block_height().unwrap_or(0); - - let created_at = doc.created_at().unwrap_or(0); - - let sender_id = doc.owner_id(); - - Ok(ContactRequest::new( - sender_id, - to_user_id, - sender_key_index, - recipient_key_index, - account_reference, - encrypted_public_key, - created_at_core_block_height, - created_at, - )) -} diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs index 4c273f341f6..78b2076c4ae 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs @@ -1,10 +1,15 @@ +use crate::error::PlatformWalletError; +use crate::ContactRequest; use crate::IdentityManager; +use dpp::prelude::Identifier; use key_wallet::wallet::ManagedWalletInfo; use key_wallet::Network; use std::fmt; mod accessors; mod contact_requests; +mod identity_discovery; +pub(crate) mod key_derivation; mod managed_account_operations; mod matured_transactions; mod wallet_info_interface; @@ -49,6 +54,95 @@ impl fmt::Debug for PlatformWalletInfo { } } +/// Parse a contact request document into a ContactRequest struct +/// +/// Extracts DashPay contact request fields from a platform document. +pub(super) fn parse_contact_request_document( + doc: &dpp::document::Document, +) -> Result { + use dpp::document::DocumentV0Getters; + use dpp::platform_value::Value; + + let properties = doc.properties(); + + let to_user_id = properties + .get("toUserId") + .and_then(|v| match v { + Value::Identifier(id) => Some(Identifier::from(*id)), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid toUserId in contact request".to_string(), + ) + })?; + + let sender_key_index = properties + .get("senderKeyIndex") + .and_then(|v| match v { + Value::U32(i) => Some(*i), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid senderKeyIndex in contact request".to_string(), + ) + })?; + + let recipient_key_index = properties + .get("recipientKeyIndex") + .and_then(|v| match v { + Value::U32(i) => Some(*i), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid recipientKeyIndex in contact request".to_string(), + ) + })?; + + let account_reference = properties + .get("accountReference") + .and_then(|v| match v { + Value::U32(i) => Some(*i), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid accountReference in contact request".to_string(), + ) + })?; + + let encrypted_public_key = properties + .get("encryptedPublicKey") + .and_then(|v| match v { + Value::Bytes(b) => Some(b.clone()), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid encryptedPublicKey in contact request".to_string(), + ) + })?; + + let created_at_core_block_height = doc.created_at_core_block_height().unwrap_or(0); + + let created_at = doc.created_at().unwrap_or(0); + + let sender_id = doc.owner_id(); + + Ok(ContactRequest::new( + sender_id, + to_user_id, + sender_key_index, + recipient_key_index, + account_reference, + encrypted_public_key, + created_at_core_block_height, + created_at, + )) +} + #[cfg(test)] mod tests { use crate::platform_wallet_info::PlatformWalletInfo; From ae08123183d86c8da68fbd053d713649a639ca09 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 6 Mar 2026 04:49:42 -0600 Subject: [PATCH 2/3] refactor(platform-wallet): extract shared contact request logic and use tracing - Extract duplicated contact request fetch+store loops from identity_discovery.rs and matured_transactions.rs into a shared fetch_and_store_contact_requests() helper method on PlatformWalletInfo - Replace all eprintln! calls with tracing::warn! for proper library logging (add tracing dependency) - Extract magic number 100 into DEFAULT_CONTACT_REQUEST_LIMIT constant - Log silently-swallowed contact request parse errors instead of ignoring them (aids debugging malformed documents) --- Cargo.lock | 1 + packages/rs-platform-wallet/Cargo.toml | 1 + .../identity_discovery.rs | 55 +++----------- .../matured_transactions.rs | 57 +++----------- .../src/platform_wallet_info/mod.rs | 74 +++++++++++++++++++ .../wallet_transaction_checker.rs | 2 +- 6 files changed, 100 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d01ca90db8a..8035ecf34fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4881,6 +4881,7 @@ dependencies = [ "platform-encryption", "rand 0.8.5", "thiserror 1.0.69", + "tracing", ] [[package]] diff --git a/packages/rs-platform-wallet/Cargo.toml b/packages/rs-platform-wallet/Cargo.toml index c30e7e43e9a..0725b6a84d1 100644 --- a/packages/rs-platform-wallet/Cargo.toml +++ b/packages/rs-platform-wallet/Cargo.toml @@ -22,6 +22,7 @@ dashcore = { workspace = true } # Standard dependencies thiserror = "1.0" async-trait = "0.1" +tracing = "0.1" # Collections indexmap = "2.0" diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs index fafd2d89b82..f624562f06f 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs @@ -5,7 +5,6 @@ //! wallet's BIP32 tree and queries Platform to find registered identities. use super::key_derivation::derive_identity_auth_key_hash; -use super::parse_contact_request_document; use super::PlatformWalletInfo; use crate::error::PlatformWalletError; use dpp::identity::accessors::IdentityGettersV0; @@ -58,8 +57,7 @@ impl PlatformWalletInfo { while consecutive_misses < gap_limit { // Derive the authentication key hash for this identity index (key_index 0) - let key_hash_array = - derive_identity_auth_key_hash(wallet, network, identity_index, 0)?; + let key_hash_array = derive_identity_auth_key_hash(wallet, network, identity_index, 0)?; // Query Platform for an identity registered with this key hash match dpp::identity::Identity::fetch(&sdk, PublicKeyHash(key_hash_array)).await { @@ -82,9 +80,10 @@ impl PlatformWalletInfo { consecutive_misses += 1; } Err(e) => { - eprintln!( - "Failed to query identity by public key hash at index {}: {}", - identity_index, e + tracing::warn!( + identity_index, + "Failed to query identity by public key hash: {}", + e ); consecutive_misses += 1; } @@ -142,45 +141,15 @@ impl PlatformWalletInfo { None => continue, }; - match sdk - .fetch_all_contact_requests_for_identity(&identity, Some(100)) + if let Err(e) = self + .fetch_and_store_contact_requests(&sdk, &identity, identity_id) .await { - Ok((sent_docs, received_docs)) => { - // Process sent contact requests - for (_doc_id, maybe_doc) in sent_docs { - if let Some(doc) = maybe_doc { - if let Ok(contact_request) = parse_contact_request_document(&doc) { - if let Some(managed_identity) = self - .identity_manager_mut() - .managed_identity_mut(identity_id) - { - managed_identity.add_sent_contact_request(contact_request); - } - } - } - } - - // Process received contact requests - for (_doc_id, maybe_doc) in received_docs { - if let Some(doc) = maybe_doc { - if let Ok(contact_request) = parse_contact_request_document(&doc) { - if let Some(managed_identity) = self - .identity_manager_mut() - .managed_identity_mut(identity_id) - { - managed_identity.add_incoming_contact_request(contact_request); - } - } - } - } - } - Err(e) => { - eprintln!( - "Failed to fetch contact requests for identity {}: {}", - identity_id, e - ); - } + tracing::warn!( + %identity_id, + "Failed to fetch contact requests during discovery: {}", + e + ); } } diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs b/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs index 4c23665b3e1..c80f755b628 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs @@ -4,7 +4,6 @@ //! asset lock transactions. use super::key_derivation::derive_identity_auth_key_hash; -use super::parse_contact_request_document; use super::PlatformWalletInfo; use crate::error::PlatformWalletError; use dpp::identity::accessors::IdentityGettersV0; @@ -81,8 +80,7 @@ impl PlatformWalletInfo { .clone(); // Derive the first authentication key hash (identity_index 0, key_index 0) - let key_hash_array = - derive_identity_auth_key_hash(wallet, self.network(), 0, 0)?; + let key_hash_array = derive_identity_auth_key_hash(wallet, self.network(), 0, 0)?; // Query Platform for identity by public key hash match dpp::identity::Identity::fetch(&sdk, PublicKeyHash(key_hash_array)).await { @@ -99,57 +97,24 @@ impl PlatformWalletInfo { } // Fetch DashPay contact requests for this identity - match sdk - .fetch_all_contact_requests_for_identity(&identity, Some(100)) + if let Err(e) = self + .fetch_and_store_contact_requests(&sdk, &identity, &identity_id) .await { - Ok((sent_docs, received_docs)) => { - // Process sent contact requests - for (_doc_id, maybe_doc) in sent_docs { - if let Some(doc) = maybe_doc { - if let Ok(contact_request) = parse_contact_request_document(&doc) { - // Add to managed identity - if let Some(managed_identity) = self - .identity_manager_mut() - .managed_identity_mut(&identity_id) - { - managed_identity.add_sent_contact_request(contact_request); - } - } - } - } - - // Process received contact requests - for (_doc_id, maybe_doc) in received_docs { - if let Some(doc) = maybe_doc { - if let Ok(contact_request) = parse_contact_request_document(&doc) { - // Add to managed identity - if let Some(managed_identity) = self - .identity_manager_mut() - .managed_identity_mut(&identity_id) - { - managed_identity - .add_incoming_contact_request(contact_request); - } - } - } - } - - identities_processed.push(identity_id); - } - Err(e) => { - eprintln!( - "Failed to fetch contact requests for identity {}: {}", - identity_id, e - ); - } + tracing::warn!( + %identity_id, + "Failed to fetch contact requests after asset lock: {}", + e + ); + } else { + identities_processed.push(identity_id); } } Ok(None) => { // No identity found for this key - that's ok, may not be registered yet } Err(e) => { - eprintln!("Failed to query identity by public key hash: {}", e); + tracing::warn!("Failed to query identity by public key hash: {}", e); } } diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs index 78b2076c4ae..9bd87af27fc 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs @@ -15,6 +15,9 @@ mod matured_transactions; mod wallet_info_interface; mod wallet_transaction_checker; +/// Default maximum number of contact request documents to fetch per identity. +const DEFAULT_CONTACT_REQUEST_LIMIT: u32 = 100; + /// Platform wallet information that extends ManagedWalletInfo with identity support #[derive(Clone)] pub struct PlatformWalletInfo { @@ -54,6 +57,77 @@ impl fmt::Debug for PlatformWalletInfo { } } +impl PlatformWalletInfo { + /// Fetch and store DashPay contact requests for a single identity. + /// + /// Queries Platform for sent and received contact request documents, + /// parses them, and stores them on the corresponding managed identity. + pub(super) async fn fetch_and_store_contact_requests( + &mut self, + sdk: &dash_sdk::Sdk, + identity: &dpp::identity::Identity, + identity_id: &Identifier, + ) -> Result<(), PlatformWalletError> { + let (sent_docs, received_docs) = sdk + .fetch_all_contact_requests_for_identity(identity, Some(DEFAULT_CONTACT_REQUEST_LIMIT)) + .await + .map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!( + "Failed to fetch contact requests for identity {}: {}", + identity_id, e + )) + })?; + + // Process sent contact requests + for (_doc_id, maybe_doc) in sent_docs { + if let Some(doc) = maybe_doc { + match parse_contact_request_document(&doc) { + Ok(contact_request) => { + if let Some(managed_identity) = self + .identity_manager_mut() + .managed_identity_mut(identity_id) + { + managed_identity.add_sent_contact_request(contact_request); + } + } + Err(e) => { + tracing::warn!( + identity_id = %identity_id, + "Failed to parse sent contact request document: {}", + e + ); + } + } + } + } + + // Process received contact requests + for (_doc_id, maybe_doc) in received_docs { + if let Some(doc) = maybe_doc { + match parse_contact_request_document(&doc) { + Ok(contact_request) => { + if let Some(managed_identity) = self + .identity_manager_mut() + .managed_identity_mut(identity_id) + { + managed_identity.add_incoming_contact_request(contact_request); + } + } + Err(e) => { + tracing::warn!( + identity_id = %identity_id, + "Failed to parse received contact request document: {}", + e + ); + } + } + } + } + + Ok(()) + } +} + /// Parse a contact request document into a ContactRequest struct /// /// Extracts DashPay contact request fields from a platform document. diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs b/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs index 7c5de27928d..1d27e593e17 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs @@ -37,7 +37,7 @@ impl WalletTransactionChecker for PlatformWalletInfo { .fetch_identity_and_contacts_for_asset_lock(wallet, tx) .await { - eprintln!("Failed to fetch identity for asset lock: {}", e); + tracing::warn!("Failed to fetch identity for asset lock: {}", e); } } } From 7e87f5b6ef4b970bd4c762687479f28b0c105487 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 6 Mar 2026 07:09:25 -0600 Subject: [PATCH 3/3] fix(platform-wallet): address CodeRabbit review feedback - Only push to discovered list when identity is actually new (avoids reprocessing existing identities in discover_identities_with_contacts) - Use identity() instead of identities().contains_key() to avoid cloning the entire identities map - Don't increment consecutive_misses on query failures (a network/SDK error is not a confirmed empty slot and shouldn't consume gap budget) - Treat missing created_at and created_at_core_block_height as errors instead of silently coercing to 0, consistent with other required contact request fields --- .../src/platform_wallet_info/identity_discovery.rs | 12 ++++-------- .../src/platform_wallet_info/mod.rs | 14 +++++++++++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs index f624562f06f..3b50af51620 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs @@ -65,15 +65,10 @@ impl PlatformWalletInfo { let identity_id = identity.id(); // Add to manager if not already present - if !self - .identity_manager() - .identities() - .contains_key(&identity_id) - { + if self.identity_manager().identity(&identity_id).is_none() { self.identity_manager_mut().add_identity(identity)?; + discovered.push(identity_id); } - - discovered.push(identity_id); consecutive_misses = 0; } Ok(None) => { @@ -85,7 +80,8 @@ impl PlatformWalletInfo { "Failed to query identity by public key hash: {}", e ); - consecutive_misses += 1; + // Don't increment consecutive_misses: a query failure is not + // a confirmed empty slot and should not consume the gap budget. } } diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs index 9bd87af27fc..1efff631092 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs @@ -199,9 +199,17 @@ pub(super) fn parse_contact_request_document( ) })?; - let created_at_core_block_height = doc.created_at_core_block_height().unwrap_or(0); - - let created_at = doc.created_at().unwrap_or(0); + let created_at_core_block_height = doc.created_at_core_block_height().ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing created_at_core_block_height in contact request".to_string(), + ) + })?; + + let created_at = doc.created_at().ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing created_at in contact request".to_string(), + ) + })?; let sender_id = doc.owner_id();