Merge pull request #3070 from mstrofnone/fix/namecoin-search-id-d-prefixes

fix(search): route d/ and id/ Namecoin namespaces through the resolution row
This commit is contained in:
Vitor Pamplona
2026-05-27 15:06:10 -04:00
committed by GitHub
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