fix(search): route d/ and id/ Namecoin namespaces through the resolution row

The inline Namecoin resolution row in the global search bar (and the
on-chain zap recipient field) is gated by looksLikeNamecoinIdentifier,
which previously only matched the '.bit' shapes. Direct namespace
references like 'id/mstrofnone' or 'd/mstrofnone' — both accepted by
NamecoinNameResolver.isNamecoinIdentifier — fell through the gate and
the resolver was never called, so no on-chain feedback appeared in the
search bar.

Bring the UI gate in line with the resolver:

  - accept 'd/<name>' (domain namespace, direct reference)
  - accept 'id/<name>' (identity namespace)
  - lower the length floor so single-character labels (valid, if
    expensive, on chain) still trigger; '.bit' inputs keep their
    5-char floor

Doc comment for the row's behaviour and the NamecoinResolutionRow
KDoc are updated to reflect the broader accepted set. New unit tests
cover both new prefixes (with case-insensitivity and leading '@'),
short single-label names, bare-prefix rejection, and explicit
non-routing for other Namecoin namespaces ('a/', 'u/').
This commit is contained in:
m
2026-05-28 04:46:55 +10:00
parent d5cca1e3e4
commit 216176eeb7
2 changed files with 60 additions and 1 deletions
@@ -102,9 +102,22 @@ fun mapOutcomeToResolveState(outcome: NamecoinResolveOutcome): NamecoinResolveSt
* Lightweight syntactic check: does this look like something we should
* route to Namecoin? Mirrors [com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver.isNamecoinIdentifier]
* but tolerates a leading `@` (matches the dropdown's [com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState.userSearchTermOrNull]).
*
* Accepted shapes:
* - `host.bit` and `user@host.bit` (domain namespace)
* - `d/<name>` (domain namespace, direct reference)
* - `id/<name>` (identity namespace)
*
* The length floor matches the dropdown's `>2 chars` rule while still
* requiring at least one character after a `d/`/`id/` prefix, so single-
* character Namecoin labels (which are valid, if expensive, on chain)
* still trigger the on-chain lookup.
*/
fun looksLikeNamecoinIdentifier(raw: String): Boolean {
val trimmed = raw.trim().removePrefix("@").lowercase()
if (trimmed.length < 3) return false
if (trimmed.startsWith("d/") && trimmed.length > 2) return true
if (trimmed.startsWith("id/") && trimmed.length > 3) return true
if (trimmed.length < 5) return false
return trimmed.endsWith(".bit") ||
trimmed.contains("@") && trimmed.substringAfter('@').endsWith(".bit")
@@ -117,7 +130,8 @@ fun looksLikeNamecoinIdentifier(raw: String): Boolean {
* field and the global search bar both have this race).
*
* Behaviour:
* - Renders nothing when [searchInput] is not a `.bit` identifier.
* - Renders nothing when [searchInput] is not a Namecoin-shaped
* identifier (`.bit`, `d/<name>`, or `id/<name>`).
* - Shows a small spinner row ("Resolving on Namecoin…") while the
* ElectrumX lookup is in flight (after a 300 ms debounce to match
* typical input-field debounce intervals).
@@ -90,6 +90,51 @@ class NamecoinResolutionRowTest {
assertFalse(looksLikeNamecoinIdentifier("foo@bar.com"))
}
@Test
fun `d slash name is a namecoin identifier`() {
// Direct domain-namespace references are accepted both via the
// search bar (mirrors NamecoinNameResolver.isNamecoinIdentifier)
// and via the dropdown's leading-at-sign convention.
assertTrue(looksLikeNamecoinIdentifier("d/mstrofnone"))
assertTrue(looksLikeNamecoinIdentifier("D/Example"))
assertTrue(looksLikeNamecoinIdentifier("@d/mstrofnone"))
}
@Test
fun `id slash name is a namecoin identifier`() {
// Identity-namespace references must also flow to the resolver;
// the parser at NamecoinNameResolver.parseIdentifier maps these
// to (id/<name>, localPart="_").
assertTrue(looksLikeNamecoinIdentifier("id/mstrofnone"))
assertTrue(looksLikeNamecoinIdentifier("ID/Alice"))
assertTrue(looksLikeNamecoinIdentifier("@id/alice"))
}
@Test
fun `short single-label d and id names still trigger`() {
// Namecoin allows single-character labels in both namespaces;
// "d/a" / "id/a" should not be filtered out by the length floor.
assertTrue(looksLikeNamecoinIdentifier("d/a"))
assertTrue(looksLikeNamecoinIdentifier("id/a"))
}
@Test
fun `bare namespace prefix with no name does not match`() {
// "d/" and "id/" alone are not lookups.
assertFalse(looksLikeNamecoinIdentifier("d/"))
assertFalse(looksLikeNamecoinIdentifier("id/"))
}
@Test
fun `other namecoin namespaces are not surfaced by the search row`() {
// The Nostr-NIP05 contract only spans the domain (d/) and
// identity (id/) namespaces. Other Namecoin namespaces exist on
// chain but aren't part of the resolution surface, so the row
// should stay quiet.
assertFalse(looksLikeNamecoinIdentifier("a/somealias"))
assertFalse(looksLikeNamecoinIdentifier("u/someuser"))
}
// ── mapOutcomeToResolveState ───────────────────────────────────────────
// Reuses the shared NamecoinResolveState already used by
// NamecoinNameService and the desktop SearchScreen so all surfaces