Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
186 changes: 146 additions & 40 deletions src/backend_task/dashpay/contact_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,24 +184,54 @@ pub async fn send_contact_request_with_proof(
// Step 1: Resolve the recipient identity
let to_identity = if to_username_or_id.ends_with(".dash") {
// It's a complete username, resolve via DPNS
resolve_username_to_identity(sdk, &to_username_or_id).await?
resolve_username_to_identity(app_context, sdk, &to_username_or_id).await?
} else {
// Try to parse as identity ID first
match Identifier::from_string_try_encodings(
&to_username_or_id,
&[Encoding::Base58, Encoding::Hex],
) {
Ok(to_id) => {
// Successfully parsed as ID, fetch the identity
Identity::fetch(sdk, to_id)
.await
.map_err(|e| format!("Failed to fetch identity: {}", e))?
.ok_or_else(|| format!("Identity {} not found", to_username_or_id))?
// Successfully parsed as ID, fetch the identity with retry
// logic for transient platform errors.
const MAX_RETRIES: u32 = 3;
let mut retries = 0u32;
loop {
match Identity::fetch(sdk, to_id).await {
Ok(Some(identity)) => break identity,
Ok(None) => {
return Err(format!("Identity {} not found", to_username_or_id));
}
Err(e) => {
let err = e.to_string();
if (err.contains("try another server")
|| err.contains("height is outdated"))
&& retries < MAX_RETRIES
{
retries += 1;
tracing::warn!(
"Retrying identity fetch for '{}' (attempt {}/{}): {}",
to_username_or_id,
retries,
MAX_RETRIES,
e
);
continue;
}
if err.contains("height is outdated")
|| err.contains("try another server")
{
return Err("Platform servers are temporarily out of sync. Please try again in a moment.".to_string());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
return Err(format!("Failed to fetch identity: {}", e));
}
}
}
Comment on lines +207 to +232

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Retry logic copy-pasted 3 times with no backoff

The same retry loop pattern (loop + is_transient_platform_sync_error check + increment + continue) is duplicated at lines 207-232, 575-596, and 633-659. Extract a generic async retry helper to reduce maintenance drift risk.

source: ['claude-general']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/backend_task/dashpay/contact_requests.rs`:
- [SUGGESTION] lines 207-232: Retry logic copy-pasted 3 times with no backoff
  The same retry loop pattern (loop + is_transient_platform_sync_error check + increment + continue) is duplicated at lines 207-232, 575-596, and 633-659. Extract a generic async retry helper to reduce maintenance drift risk.

}
Err(_) => {
// Not a valid ID format, assume it's a username without .dash suffix
let username_with_suffix = format!("{}.dash", to_username_or_id);
resolve_username_to_identity(sdk, &username_with_suffix).await?
resolve_username_to_identity(app_context, sdk, &username_with_suffix).await?
}
}
};
Expand Down Expand Up @@ -504,38 +534,65 @@ pub async fn send_contact_request_with_proof(
))
}

async fn resolve_username_to_identity(sdk: &Sdk, username: &str) -> Result<Identity, String> {
async fn resolve_username_to_identity(
app_context: &Arc<AppContext>,
sdk: &Sdk,
username: &str,
) -> Result<Identity, String> {
// Parse username (e.g., "alice.dash" -> "alice")
let name = username
.split('.')
.next()
.ok_or_else(|| format!("Invalid username format: {}", username))?;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Query DPNS for the username
let dpns_contract_id = Identifier::from_string(
"GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec",
Encoding::Base58,
)
.map_err(|e| format!("Failed to parse DPNS contract ID: {}", e))?;

let dpns_contract = dash_sdk::platform::DataContract::fetch(sdk, dpns_contract_id)
.await
.map_err(|e| format!("Failed to fetch DPNS contract: {}", e))?
.ok_or("DPNS contract not found")?;

let mut query = DocumentQuery::new(Arc::new(dpns_contract), "domain")
.map_err(|e| format!("Failed to create DPNS query: {}", e))?;

query = query.with_where(WhereClause {
field: "normalizedLabel".to_string(),
operator: WhereOperator::Equal,
value: Value::Text(name.to_lowercase()),
});
query.limit = 1;

let results = Document::fetch_many(sdk, query)
.await
.map_err(|e| format!("Failed to query DPNS: {}", e))?;
// Normalize the label using homograph-safe conversion, consistent with DPNS registration
let normalized_name =
dash_sdk::dpp::util::strings::convert_to_homograph_safe_chars(&name.to_lowercase());

// Query DPNS for the username using the app context's cached contract.
// Retry on transient "height is outdated" / "try another server" errors.
const MAX_RETRIES: u32 = 3;
let dpns_contract = app_context.dpns_contract.clone();

let mut retries = 0u32;
let results = loop {
let query = DocumentQuery::new(dpns_contract.clone(), "domain")
.map_err(|e| format!("Failed to create DPNS query: {}", e))?
.with_where(WhereClause {
field: "normalizedParentDomainName".to_string(),
operator: WhereOperator::Equal,
value: Value::Text("dash".to_string()),
})
.with_where(WhereClause {
field: "normalizedLabel".to_string(),
operator: WhereOperator::Equal,
value: Value::Text(normalized_name.clone()),
});

match Document::fetch_many(sdk, query).await {
Ok(results) => break results,
Err(e) => {
let err = e.to_string();
if (err.contains("try another server") || err.contains("height is outdated"))
&& retries < MAX_RETRIES
{
retries += 1;
tracing::warn!(
"Retrying DPNS query for '{}' (attempt {}/{}): {}",
username,
retries,
MAX_RETRIES,
e
);
continue;
}
if err.contains("height is outdated") || err.contains("try another server") {
return Err("Platform servers are temporarily out of sync. Please try again in a moment.".to_string());
}
return Err(format!("Failed to query DPNS: {}", e));
}
}
};

let (_, document) = results
.into_iter()
Expand All @@ -544,14 +601,63 @@ async fn resolve_username_to_identity(sdk: &Sdk, username: &str) -> Result<Ident

let document = document.ok_or_else(|| format!("Invalid DPNS document for '{}'", username))?;

// Get the identity ID from the DPNS document
let identity_id = document.owner_id();
// Extract the identity ID from records.identity (not owner_id, which may differ)
let identity_id = document
.get("records")
.and_then(|records| {
if let Value::Map(map) = records {
map.iter()
.find(|(k, _)| matches!(k, Value::Text(key) if key == "identity"))
.map(|(_, v)| v.clone())
} else {
None
}
})
.and_then(|id_value| {
if let Value::Identifier(id_bytes) = id_value {
Some(Identifier::from(id_bytes))
} else {
None
}
})
.ok_or_else(|| {
format!(
"DPNS document for '{}' does not contain a valid identity reference",
username
)
})?;

// Fetch the identity
Identity::fetch(sdk, identity_id)
.await
.map_err(|e| format!("Failed to fetch identity for '{}': {}", username, e))?
.ok_or_else(|| format!("Identity not found for username '{}'", username))
// Fetch the identity with retry logic for transient errors
let mut retries = 0u32;
loop {
match Identity::fetch(sdk, identity_id).await {
Ok(Some(identity)) => return Ok(identity),
Ok(None) => return Err(format!("Identity not found for username '{}'", username)),
Err(e) => {
let err = e.to_string();
if (err.contains("try another server") || err.contains("height is outdated"))
&& retries < MAX_RETRIES
{
retries += 1;
tracing::warn!(
"Retrying identity fetch for '{}' (attempt {}/{}): {}",
username,
retries,
MAX_RETRIES,
e
);
continue;
}
if err.contains("height is outdated") || err.contains("try another server") {
return Err("Platform servers are temporarily out of sync. Please try again in a moment.".to_string());
}
return Err(format!(
"Failed to fetch identity for '{}': {}",
username, e
));
}
}
}
}

pub async fn accept_contact_request(
Expand Down
13 changes: 11 additions & 2 deletions src/backend_task/dashpay/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,17 @@ impl DashPayError {
DashPayError::QrCodeExpired { .. } => {
"QR code has expired. Please ask for a new one.".to_string()
}
DashPayError::NetworkError { .. } => {
"Network connection error. Please check your internet connection.".to_string()
DashPayError::NetworkError { reason } => {
// Surface curated reason when it's a known platform-sync message;
// fall back to generic network text for everything else.
if reason.contains("temporarily out of sync")
|| reason.contains("height is outdated")
|| reason.contains("try another server")
{
reason.clone()
} else {
"Network connection error. Please check your internet connection.".to_string()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
DashPayError::ValidationFailed { errors } => {
if errors.len() == 1 {
Expand Down
7 changes: 7 additions & 0 deletions src/ui/dashpay/add_contact_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,13 @@ impl ScreenLike for AddContactScreen {
)
.unwrap_or_else(|_| dash_sdk::platform::Identifier::random()),
}
} else if message.contains("try another server")
|| message.contains("height is outdated")
|| message.contains("temporarily out of sync")
{
DashPayError::NetworkError {
reason: "Platform servers are temporarily out of sync. Please try again in a moment.".to_string(),
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else if message.contains("Network") || message.contains("connection") {
DashPayError::NetworkError {
reason: message.clone(),
Expand Down