Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 50 additions & 15 deletions src/backend_task/dashpay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub mod validation;
pub use contacts::ContactData;

use crate::model::qualified_identity::QualifiedIdentity;
use dash_sdk::dpp::identity::accessors::IdentityGettersV0;
use dash_sdk::dpp::platform_value::string_encoding::Encoding;
use dash_sdk::platform::{Identifier, IdentityPublicKey};

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -165,21 +167,54 @@ impl AppContext {
identity,
request_id,
} => contact_requests::reject_contact_request(self, sdk, identity, request_id).await,
DashPayTask::LoadPaymentHistory { identity: _ } => {
// TODO: Implement payment history loading according to DIP-0015
// This requires an SPV client to query the blockchain, which is not yet available.
// Once SPV support is added, the implementation would:
// 1. Get all established contacts (bidirectional contact requests)
// 2. For each contact, derive payment addresses from their encrypted extended public key
// 3. Query blockchain via SPV for transactions to/from those addresses
// 4. Build payment history records with amount, timestamp, memo, etc.
// 5. Store in local database for faster access
//
// The derivation path for DashPay addresses is:
// m/9'/5'/15'/account'/(our_identity_id)/(contact_identity_id)/index
//
// For now, return empty payment history until SPV client is available
Ok(BackendTaskSuccessResult::DashPayPaymentHistory(Vec::new()))
DashPayTask::LoadPaymentHistory { identity } => {
// Load locally stored payment records from database.
// Full blockchain-based history (scanning DIP-15 addresses via SPV)
// remains deferred until SPV support is available.
let identity_id = identity.identity.id();
let stored = self
.db
.load_payment_history(&identity_id, 100)
Comment thread
lklimek marked this conversation as resolved.
Resolved
.map_err(|e| format!("Failed to load payment history: {}", e))?;

let network_str = self.network.to_string();
let contacts = self
.db
.load_dashpay_contacts(&identity_id, &network_str)
.unwrap_or_default();

let mut results = Vec::new();
for sp in stored {
let is_incoming = sp.to_identity_id == identity_id.to_buffer().to_vec();
let contact_bytes = if is_incoming {
&sp.from_identity_id
} else {
&sp.to_identity_id
};

let contact_name = contacts
.iter()
.find(|c| c.contact_identity_id == *contact_bytes)
.and_then(|c| c.username.clone().or(c.display_name.clone()))
.unwrap_or_else(|| {
if let Ok(cid) = Identifier::from_bytes(contact_bytes) {
let s = cid.to_string(Encoding::Base58);
format!("Unknown ({})", &s[..s.len().min(8)])
} else {
"Unknown".to_string()
}
});

results.push((
sp.tx_id,
contact_name,
sp.amount as u64,
Comment thread
lklimek marked this conversation as resolved.
Resolved
is_incoming,
sp.memo.unwrap_or_default(),
));
}

Ok(BackendTaskSuccessResult::DashPayPaymentHistory(results))
}
DashPayTask::SendPaymentToContact {
identity,
Expand Down
5 changes: 0 additions & 5 deletions src/backend_task/dashpay/contact_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,6 @@ pub async fn load_contact_requests(
tracing::info!("Fetched {} outgoing documents", outgoing_docs.len());

// Convert to vec of tuples (id, document)
// TODO: Process autoAcceptProof for incoming requests
// When an incoming request has a valid autoAcceptProof, we should:
// 1. Verify the proof signature
// 2. Automatically send a contact request back if valid
// 3. Mark the contact as auto-accepted
let mut incoming: Vec<(Identifier, Document)> = incoming_docs
.into_iter()
.filter_map(|(id, doc)| doc.map(|d| (id, d)))
Expand Down
150 changes: 78 additions & 72 deletions src/backend_task/dashpay/contacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use dash_sdk::dpp::key_wallet::bip32::{ChildNumber, DerivationPath, ExtendedPriv
use dash_sdk::dpp::platform_value::Value;
use dash_sdk::drive::query::{OrderClause, WhereClause, WhereOperator};
use dash_sdk::platform::{Document, DocumentQuery, Fetch, FetchMany, Identifier};
use futures::future::join_all;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
Expand Down Expand Up @@ -409,81 +410,86 @@ pub async fn load_contacts(
})
.collect();

// Fetch profiles and usernames for all contacts
// First, collect all contact IDs
let contact_ids: Vec<Identifier> = contact_list.iter().map(|c| c.identity_id).collect();

// Fetch profiles for all contacts (batch query)
if !contact_ids.is_empty() {
// Query profiles for all contacts
for contact_id in &contact_ids {
// Fetch profile
let mut profile_query = DocumentQuery::new(dashpay_contract.clone(), "profile")
.map_err(|e| format!("Failed to create profile query: {}", e))?;

profile_query = profile_query.with_where(WhereClause {
field: "$ownerId".to_string(),
operator: WhereOperator::Equal,
value: Value::Identifier(contact_id.to_buffer()),
});
profile_query.limit = 1;

if let Ok(results) = Document::fetch_many(sdk, profile_query).await
&& let Some((_, Some(doc))) = results.into_iter().next()
{
let props = doc.properties();

let display_name = props
.get("displayName")
.and_then(|v| v.as_text())
.map(|s| s.to_string());

let avatar_url = props
.get("avatarUrl")
.and_then(|v| v.as_text())
.map(|s| s.to_string());

let bio = props
.get("bio")
.and_then(|v| v.as_text())
.map(|s| s.to_string());

// Update the contact in the list
if let Some(contact) = contact_list
.iter_mut()
.find(|c| c.identity_id == *contact_id)
{
contact.display_name = display_name;
contact.avatar_url = avatar_url;
contact.bio = bio;
}
}

// Fetch DPNS username
let dpns_contract = app_context.dpns_contract.clone();
let mut dpns_query = DocumentQuery::new(dpns_contract, "domain")
.map_err(|e| format!("Failed to create DPNS query: {}", e))?;

dpns_query = dpns_query.with_where(WhereClause {
field: "records.identity".to_string(),
operator: WhereOperator::Equal,
value: Value::Identifier(contact_id.to_buffer()),
});
dpns_query.limit = 1;
// Fetch profiles and usernames for all contacts in parallel with bounded concurrency.
// Each contact requires two network queries (profile + DPNS username), so parallelizing
// in chunks significantly reduces total load time for large contact lists.
const CHUNK_SIZE: usize = 10;

for chunk in contact_list.chunks_mut(CHUNK_SIZE) {
let futures: Vec<_> = chunk
.iter()
.map(|contact| {
let dashpay_contract = dashpay_contract.clone();
let dpns_contract = app_context.dpns_contract.clone();
let contact_id = contact.identity_id;

async move {
let mut display_name = None;
let mut avatar_url = None;
let mut bio = None;
let mut username = None;

// Fetch profile
if let Ok(mut profile_query) = DocumentQuery::new(dashpay_contract, "profile") {
profile_query = profile_query.with_where(WhereClause {
field: "$ownerId".to_string(),
operator: WhereOperator::Equal,
value: Value::Identifier(contact_id.to_buffer()),
});
profile_query.limit = 1;

if let Ok(results) = Document::fetch_many(sdk, profile_query).await
&& let Some((_, Some(doc))) = results.into_iter().next()
{
let props = doc.properties();
display_name = props
.get("displayName")
.and_then(|v| v.as_text())
.map(|s| s.to_string());
avatar_url = props
.get("avatarUrl")
.and_then(|v| v.as_text())
.map(|s| s.to_string());
bio = props
.get("bio")
.and_then(|v| v.as_text())
.map(|s| s.to_string());
}
}

if let Ok(results) = Document::fetch_many(sdk, dpns_query).await
&& let Some((_, Some(doc))) = results.into_iter().next()
{
let props = doc.properties();
if let Some(label) = props.get("label").and_then(|v| v.as_text()) {
// Update the contact in the list
if let Some(contact) = contact_list
.iter_mut()
.find(|c| c.identity_id == *contact_id)
{
contact.username = Some(label.to_string());
// Fetch DPNS username
if let Ok(mut dpns_query) = DocumentQuery::new(dpns_contract, "domain") {
dpns_query = dpns_query.with_where(WhereClause {
field: "records.identity".to_string(),
operator: WhereOperator::Equal,
value: Value::Identifier(contact_id.to_buffer()),
});
dpns_query.limit = 1;

if let Ok(results) = Document::fetch_many(sdk, dpns_query).await
&& let Some((_, Some(doc))) = results.into_iter().next()
Comment thread
lklimek marked this conversation as resolved.
Resolved
{
let props = doc.properties();
if let Some(label) = props.get("label").and_then(|v| v.as_text()) {
username = Some(label.to_string());
}
}
}

(contact_id, display_name, avatar_url, bio, username)
}
})
.collect();

let results = join_all(futures).await;

// Apply fetched data back to the contacts in this chunk
for (contact_id, display_name, avatar_url, bio, username) in results {
if let Some(contact) = chunk.iter_mut().find(|c| c.identity_id == contact_id) {
contact.display_name = display_name;
contact.avatar_url = avatar_url;
contact.bio = bio;
contact.username = username;
}
}
}
Expand Down
55 changes: 46 additions & 9 deletions src/backend_task/dashpay/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,20 +349,57 @@ pub async fn send_payment_to_contact_impl(

/// Load payment history from local database
pub async fn load_payment_history(
_app_context: &Arc<AppContext>,
app_context: &Arc<AppContext>,
identity_id: &Identifier,
contact_id: Option<&Identifier>,
) -> Result<Vec<PaymentRecord>, String> {
// TODO: Query local database for payment records
// Filter by identity_id and optionally by contact_id
let stored_payments = app_context
.db
.load_payment_history(identity_id, 100)
.map_err(|e| format!("Failed to load payment history: {}", e))?;

let mut records = Vec::new();
for sp in stored_payments {
let from_id = Identifier::from_bytes(&sp.from_identity_id)
.map_err(|e| format!("Invalid from_identity_id: {}", e))?;
let to_id = Identifier::from_bytes(&sp.to_identity_id)
.map_err(|e| format!("Invalid to_identity_id: {}", e))?;

// If a contact filter is specified, skip non-matching records
if let Some(filter_id) = contact_id
&& from_id != *filter_id
&& to_id != *filter_id
{
continue;
}

tracing::debug!(
"Would load payment history for identity {} with contact filter: {:?}",
identity_id.to_string(Encoding::Base58),
contact_id.map(|id| id.to_string(Encoding::Base58))
);
let status = match sp.status.as_str() {
"confirmed" => PaymentStatus::Confirmed(1),
"failed" => PaymentStatus::Failed(sp.status.clone()),
Comment thread
lklimek marked this conversation as resolved.
Resolved
"pending" => PaymentStatus::Pending,
_ => PaymentStatus::Broadcast,
};

records.push(PaymentRecord {
id: sp.id.to_string(),
from_identity: from_id,
to_identity: to_id,
from_address: None,
to_address: Address::p2pkh(
&dash_sdk::dpp::dashcore::PublicKey::from_slice(&[0x02; 33])
Comment thread
lklimek marked this conversation as resolved.
Resolved
.map_err(|e| format!("Key error: {}", e))?,
app_context.network,
),
amount: sp.amount as u64,
Comment thread
lklimek marked this conversation as resolved.
Resolved
tx_id: Some(sp.tx_id),
memo: sp.memo,
timestamp: sp.created_at as u64,
Comment thread
lklimek marked this conversation as resolved.
Resolved
status,
address_index: 0,
});
}

Ok(Vec::new())
Ok(records)
}

/// Update payment status after broadcast or confirmation
Expand Down
Loading
Loading