Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
62 changes: 47 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,51 @@ 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 } => {
// Reuse the shared payment history loader to avoid duplicating logic.
let identity_id = identity.identity.id();
let records = payments::load_payment_history(self, &identity_id, None).await?;

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

let results: Vec<_> = records
.into_iter()
.map(|rec| {
let is_incoming = rec.to_identity == identity_id;
let contact_id = if is_incoming {
rec.from_identity
} else {
rec.to_identity
};

let contact_name = contacts
.iter()
.find(|c| {
Identifier::from_bytes(&c.contact_identity_id)
.map(|id| id == contact_id)
.unwrap_or(false)
})
.and_then(|c| c.username.clone().or(c.display_name.clone()))
.unwrap_or_else(|| {
let s = contact_id.to_string(Encoding::Base58);
format!("Unknown ({})", &s[..s.len().min(8)])
});

(
rec.tx_id.unwrap_or_default(),
contact_name,
rec.amount,
is_incoming,
rec.memo.unwrap_or_default(),
)
})
.collect();

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("publicMessage")
.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("publicMessage")
.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.
{
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
83 changes: 69 additions & 14 deletions src/backend_task/dashpay/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub struct PaymentRecord {
pub from_identity: Identifier,
pub to_identity: Identifier,
pub from_address: Option<Address>,
pub to_address: Address,
pub to_address: Option<Address>,
pub amount: u64,
pub tx_id: Option<String>,
pub memo: Option<String>,
Expand Down Expand Up @@ -303,7 +303,7 @@ pub async fn send_payment_to_contact_impl(
from_identity: from_identity.identity.id(),
to_identity: to_contact_id,
from_address: None,
to_address: to_address.clone(),
to_address: Some(to_address.clone()),
amount: amount_duffs,
tx_id: Some(txid.clone()),
memo: memo.clone(),
Expand Down Expand Up @@ -349,20 +349,75 @@ 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("Transaction failed".to_string()),
"pending" => PaymentStatus::Pending,
_ => PaymentStatus::Broadcast,
};

let amount = if sp.amount < 0 {
tracing::warn!(
"Payment {} has negative amount {}, clamping to 0",
sp.id,
sp.amount
);
0u64
} else {
sp.amount as u64
};

let timestamp = if sp.created_at < 0 {
tracing::warn!(
"Payment {} has negative timestamp {}, using 0",
sp.id,
sp.created_at
);
0u64
} else {
sp.created_at as u64
};

records.push(PaymentRecord {
id: sp.id.to_string(),
from_identity: from_id,
to_identity: to_id,
from_address: None,
to_address: None,
amount,
tx_id: Some(sp.tx_id),
memo: sp.memo,
timestamp,
status,
address_index: 0,
});
}

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

/// Update payment status after broadcast or confirmation
Expand Down Expand Up @@ -412,7 +467,7 @@ mod tests {
from_identity: from_id,
to_identity: to_id,
from_address: None,
to_address: create_test_address(),
to_address: Some(create_test_address()),
amount: 100_000_000, // 1 Dash
tx_id: None,
memo: Some("Test payment".to_string()),
Expand Down Expand Up @@ -476,7 +531,7 @@ mod tests {
from_identity: Identifier::random(),
to_identity: Identifier::random(),
from_address: Some(create_test_address()),
to_address: create_test_address(),
to_address: Some(create_test_address()),
amount: 50_000_000, // 0.5 Dash
tx_id: Some("abc123def456".to_string()),
memo: None,
Expand Down Expand Up @@ -524,7 +579,7 @@ mod tests {
from_identity: Identifier::random(),
to_identity: Identifier::random(),
from_address: None,
to_address: create_test_address(),
to_address: Some(create_test_address()),
amount: 100_000_000,
tx_id: Some("tx123".to_string()),
memo: Some("Original memo".to_string()),
Expand Down
Loading
Loading