From 82b5caa76df37201e41913be83c5835873979257 Mon Sep 17 00:00:00 2001 From: mstrofnone Date: Tue, 19 May 2026 23:50:30 +1000 Subject: [PATCH] feat(namecoin): resolve .bit NIP-05 identifiers via Namecoin ElectrumX Adds optional Namecoin .bit verification to the existing NIP-05 path. Identifiers ending in .bit (and the d/ / id/ shorthands) are routed to a small browser-WSS ElectrumX client instead of HTTPS, giving users a DNS-free option for identity verification. Wire format follows the parallel ports in Amethyst (Kotlin), Nostur (Swift), nostr-tools PR #533 / Jumble PR #774 / nostrudel PR #352 (JS), and dart-nostr PR #44 (Dart, merged). Domain names use "nostr.names[]" with optional "nostr.relays" map; identity names (id/) use "nostr.pubkey" + array relays; the shorthand "nostr": "" form is accepted for root entries. - Module: src/lib/namecoin/ (cache + electrumx + parser + tests). - Single hook: src/lib/Nip05.ts wraps nostr-tools queryProfile, routing .bit identifiers and falling through for everything else. - Two call sites updated: NostrAddress.svelte (profile NIP-05 row) and the npub route slug-overwrite path. - No new top-level UI: the existing verified checkmark either lights up or stays dark, no extra badges or noise. - Browser-WSS pool is the 4-of-6 subset of amethyst's DEFAULT_ELECTRUMX_SERVERS that operators expose over WSS. Bare-IP amethyst entries are deliberately skipped (no IP-SAN cert path in browsers). Documented in a comment in electrumx.ts. - 21 new unit tests, all passing; full suite stays at 103/103. Try it: `_@mstrofnone.bit` resolves to the same pubkey as the existing mstrofnone npub. --- web/package-lock.json | 21 +- web/package.json | 1 + web/src/lib/Nip05.ts | 26 ++ web/src/lib/components/NostrAddress.svelte | 4 +- web/src/lib/namecoin/cache.ts | 52 +++ web/src/lib/namecoin/electrumx.ts | 333 ++++++++++++++++++ web/src/lib/namecoin/index.ts | 23 ++ web/src/lib/namecoin/nip05.test.ts | 313 ++++++++++++++++ web/src/lib/namecoin/nip05.ts | 243 +++++++++++++ .../(app)/[slug=npub]/(tabs)/+page.svelte | 4 +- 10 files changed, 1012 insertions(+), 8 deletions(-) create mode 100644 web/src/lib/Nip05.ts create mode 100644 web/src/lib/namecoin/cache.ts create mode 100644 web/src/lib/namecoin/electrumx.ts create mode 100644 web/src/lib/namecoin/index.ts create mode 100644 web/src/lib/namecoin/nip05.test.ts create mode 100644 web/src/lib/namecoin/nip05.ts diff --git a/web/package-lock.json b/web/package-lock.json index d36e6896..c20f7833 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emoji-mart/data": "^1.2.1", "@floating-ui/dom": "^1.7.6", + "@noble/hashes": "^2.2.0", "@rust-nostr/nostr-sdk": "^0.44.0", "@tabler/icons-svelte": "^3.41.1", "@tabler/icons-webfont": "^3.41.1", @@ -1806,12 +1807,12 @@ } }, "node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -8402,6 +8403,18 @@ } } }, + "node_modules/rx-nostr-crypto/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/rx-nostr-crypto/node_modules/nostr-typedef": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/nostr-typedef/-/nostr-typedef-0.9.0.tgz", diff --git a/web/package.json b/web/package.json index cc9e701e..a49b188f 100644 --- a/web/package.json +++ b/web/package.json @@ -51,6 +51,7 @@ "dependencies": { "@emoji-mart/data": "^1.2.1", "@floating-ui/dom": "^1.7.6", + "@noble/hashes": "^2.2.0", "@rust-nostr/nostr-sdk": "^0.44.0", "@tabler/icons-svelte": "^3.41.1", "@tabler/icons-webfont": "^3.41.1", diff --git a/web/src/lib/Nip05.ts b/web/src/lib/Nip05.ts new file mode 100644 index 00000000..fbb151c3 --- /dev/null +++ b/web/src/lib/Nip05.ts @@ -0,0 +1,26 @@ +/** + * Thin wrapper around `nostr-tools` NIP-05 lookups that routes + * Namecoin `.bit` identifiers to the chain resolver instead of HTTPS. + * + * All existing call sites use the same `{ pubkey, relays? }` shape, so + * the wrapper returns the same `ProfilePointer | null` contract. When + * the identifier is not a `.bit`-style name, the call falls through + * to `nip05.queryProfile` from `nostr-tools` unchanged. + */ +import { nip05 } from 'nostr-tools'; +import type { ProfilePointer } from 'nostr-tools/nip19'; +import { isDotBitIdentifier, queryProfile as queryNamecoin } from './namecoin'; + +/** + * Resolve a NIP-05 identifier. `.bit` (Namecoin) identifiers are + * resolved over the Namecoin chain; everything else falls through to + * nostr-tools. + */ +export async function queryProfile(identifier: string): Promise { + if (isDotBitIdentifier(identifier)) { + const result = await queryNamecoin(identifier); + if (!result) return null; + return { pubkey: result.pubkey, relays: result.relays ?? [] }; + } + return nip05.queryProfile(identifier); +} diff --git a/web/src/lib/components/NostrAddress.svelte b/web/src/lib/components/NostrAddress.svelte index 8b64ec8a..a87fddc4 100644 --- a/web/src/lib/components/NostrAddress.svelte +++ b/web/src/lib/components/NostrAddress.svelte @@ -1,6 +1,6 @@