Skip to content
Merged
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
100 changes: 76 additions & 24 deletions packages/rs-sdk/src/platform/dpns_usernames/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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<bool, Error> {
pub async fn is_dpns_name_available(&self, name: &str) -> Result<bool, Error> {
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);
}
Comment thread
lklimek marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.

let dpns_contract = self.fetch_dpns_contract().await?;

// Query for existing domain with this label
let query = DocumentQuery {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading