From 11666c6d99bb71bebbf27a003fd287ba7e14cc3d Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:00:25 +0200 Subject: [PATCH 1/2] fix(rs-sdk): case-insensitive .dash suffix in DPNS name resolution Extracted from PR #3585 (fix 1 of 2). resolve_dpns_name now compares the .dash suffix case-insensitively via eq_ignore_ascii_case, and both resolve_dpns_name and is_dpns_name_available skip the DPNS contract fetch for empty/normalized-empty labels (treating them as not-found rather than erroring or making a needless network call). Co-Authored-By: Claude Opus 4.6 --- .../rs-sdk/src/platform/dpns_usernames/mod.rs | 100 +++++++++++++----- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index c5fa757bb7e..0c32653ad6c 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -35,6 +35,27 @@ pub fn convert_to_homograph_safe_chars(input: &str) -> String { .collect() } +fn extract_dpns_label(name: &str) -> &str { + if let Some(dot_pos) = name.rfind('.') { + let (label_part, suffix) = name.split_at(dot_pos); + if suffix.eq_ignore_ascii_case(".dash") { + return label_part; + } + } + name +} + +/// Strip an optional case-insensitive `.dash` suffix and apply DPNS +/// homograph-safe normalization, producing a value suitable for matching +/// against the `normalizedLabel` field of `domain` documents. +/// +/// Accepts either a bare label (e.g. `"alice"`) or a full DPNS name +/// (e.g. `"alice.dash"`, `"Alice.DASH"`) and returns the normalized label +/// (e.g. `"a11ce"`). +fn normalize_dpns_label(input: &str) -> String { + convert_to_homograph_safe_chars(extract_dpns_label(input)) +} + /// Check if a username is valid according to DPNS rules /// /// A username is valid if: @@ -365,19 +386,31 @@ impl Sdk { /// /// # Arguments /// - /// * `label` - The username label to check (e.g., "alice") + /// * `name` - The username label (e.g., "alice") or full DPNS name + /// (e.g., "alice.dash"). The `.dash` suffix is matched + /// case-insensitively and stripped before normalization, mirroring + /// [`Sdk::resolve_dpns_name`]. /// /// # Returns /// /// Returns `true` if the name is available, `false` if it's taken - pub async fn is_dpns_name_available(&self, label: &str) -> Result { + pub async fn is_dpns_name_available(&self, name: &str) -> Result { use crate::platform::documents::document_query::DocumentQuery; use drive::query::WhereClause; use drive::query::WhereOperator; - let dpns_contract = self.fetch_dpns_contract().await?; + let normalized_label = normalize_dpns_label(name); - let normalized_label = convert_to_homograph_safe_chars(label); + // An empty normalized label (e.g. `""`, `".dash"`, `".DASH"`) is not + // a registrable DPNS name, so report it as unavailable rather than + // doing a network round-trip that would query for + // `normalizedLabel == ""`. This mirrors the early-return guard in + // `resolve_dpns_name` so the two APIs agree on malformed input. + if normalized_label.is_empty() { + return Ok(false); + } + + let dpns_contract = self.fetch_dpns_contract().await?; // Query for existing domain with this label let query = DocumentQuery { @@ -427,30 +460,15 @@ impl Sdk { use drive::query::WhereClause; use drive::query::WhereOperator; - let dpns_contract = self.fetch_dpns_contract().await?; - - // Extract label from full name if needed - // Handle both "alice" and "alice.dash" formats - let label = if let Some(dot_pos) = name.rfind('.') { - let (label_part, suffix) = name.split_at(dot_pos); - // Only strip the suffix if it's exactly ".dash" - if suffix == ".dash" { - label_part - } else { - // If it's not ".dash", treat the whole thing as the label - name - } - } else { - // No dot found, use the whole name as the label - name - }; + let normalized_label = normalize_dpns_label(name); - // Validate the label before proceeding - if label.is_empty() { + // Empty normalized label (e.g. `""`, `".dash"`) can't resolve to an + // identity; bail before the contract fetch. Mirrors `is_dpns_name_available`. + if normalized_label.is_empty() { return Ok(None); } - let normalized_label = convert_to_homograph_safe_chars(label); + let dpns_contract = self.fetch_dpns_contract().await?; // Query for domain with this label let query = DocumentQuery { @@ -509,6 +527,40 @@ mod tests { assert_eq!(convert_to_homograph_safe_chars("test123"), "test123"); } + #[test] + fn test_normalize_dpns_label_strips_dash_suffix_case_insensitively() { + // Bare label and full name normalize to the same value, regardless + // of the case of the .dash suffix. This is the contract that + // `is_dpns_name_available` and `resolve_dpns_name` share so that + // queries against `normalizedLabel` agree. + let expected = "a11ce"; + assert_eq!(normalize_dpns_label("alice"), expected); + assert_eq!(normalize_dpns_label("alice.dash"), expected); + assert_eq!(normalize_dpns_label("alice.DASH"), expected); + assert_eq!(normalize_dpns_label("Alice.DaSh"), expected); + assert_eq!(normalize_dpns_label("ALICE.DASH"), expected); + + // Non-.dash suffixes are not stripped (they are treated as part of + // the label and normalized whole). + assert_eq!(normalize_dpns_label("alice.eth"), "a11ce.eth"); + + // Empty / suffix-only inputs normalize to an empty label. + assert_eq!(normalize_dpns_label(""), ""); + assert_eq!(normalize_dpns_label(".dash"), ""); + assert_eq!(normalize_dpns_label(".DASH"), ""); + } + + #[test] + fn test_extract_dpns_label() { + assert_eq!(extract_dpns_label("alice.dash"), "alice"); + assert_eq!(extract_dpns_label("alice.DASH"), "alice"); + assert_eq!(extract_dpns_label("alice.DaSh"), "alice"); + assert_eq!(extract_dpns_label("Alice.DASH"), "Alice"); + assert_eq!(extract_dpns_label("alice"), "alice"); + assert_eq!(extract_dpns_label("alice.eth"), "alice.eth"); + assert_eq!(extract_dpns_label(".dash"), ""); + } + #[test] fn test_is_valid_username() { // Valid usernames From 371877ae9a9237c059aad736e3828b4d7136ea4a Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:16:25 +0200 Subject: [PATCH 2/2] ci: re-trigger workflows after enabling UBUNTU_BACKUP_ENABLED Empty commit to start a fresh Tests run that reads the updated UBUNTU_BACKUP_ENABLED repo variable (in-flight runs captured the old value). Co-Authored-By: Claude Opus 4.8 (1M context)