-
Notifications
You must be signed in to change notification settings - Fork 13
fix(dashpay): fix DPNS username resolution in contact requests #723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
788882d
beef9f7
e7bb91f
1873064
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()); | ||
| } | ||
| return Err(format!("Failed to fetch identity: {}", e)); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+207
to
+232
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
| 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? | ||
| } | ||
| } | ||
| }; | ||
|
|
@@ -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))?; | ||
|
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() | ||
|
|
@@ -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( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.