feat(nip05): resolve Namecoin .bit identifiers + inline search indicator#281
feat(nip05): resolve Namecoin .bit identifiers + inline search indicator#281mstrofnone wants to merge 2 commits into
Conversation
Adds a TypeScript port of the Namecoin NIP-05 wire format (ifa-0001
Domain Name Object, ElectrumX WSS transport) so that ants can verify
and look up nostr pubkeys whose identifier lives on the Namecoin
blockchain instead of DNS.
Supported identifier shapes:
- alice@example.bit
- example.bit (uses the _ root entry)
- d/example (Namecoin domain namespace)
- id/alice (Namecoin identity namespace)
- nostr: NIP-21 prefix tolerated on all of the above
What the change touches:
- src/lib/namecoin/nip05.ts new resolver + parser. WSS to an
ElectrumX server, scripthash path
for blockchain.scripthash.get_history,
NAME_UPDATE vout parsing, name
expiry check (~36000 blocks).
Pluggable WebSocket impl via
useWebSocketImplementation, BYO
fallback server list.
- src/lib/namecoin/cache.ts session-level pos/neg cache +
in-flight dedup. No localStorage,
no TTL.
- src/lib/namecoin/index.ts public-facing resolveNamecoinNip05
and isValidNamecoinNip05 helpers.
- src/lib/profile/nip05.ts route .bit identifiers through the
chain resolver (both resolveNip05ToPubkey
and verifyNip05).
- src/app/api/nip05/verify/route.ts
short-circuit .bit identifiers
through isValidNamecoinNip05 on the
server side so browsers do not have
to deal with the self-signed
ElectrumX TLS certs themselves.
- src/components/NamecoinResolutionIndicator.tsx
inline indicator mounted under the
global search bar. Renders a
spinner, then either the resolved
npub + Namecoin badge (clickable
into /p/<npub>) or an explicit
failure message.
- src/components/SearchView.tsx mount the indicator between
<SearchInput> and <QueryTranslation>.
Tests:
src/lib/namecoin/__tests__/nip05.test.ts adds 19 cases covering
identifier parsing (.bit / d/ / id/, nostr: prefix, user@domain.bit),
value-JSON extraction (simple form, extended names map, id/ identity
object, malformed input), relay surfacing, and import-walking helper.
Verification:
npx tsc --noEmit clean
npm run lint clean (next lint + tsc --noEmit)
npx jest 50/50 passing (19 new + 31 existing)
npm run build production build green
Wire format reference:
- NIP-2349 (Namecoin NIP-05) draft.
- amethyst PR #2956 / #2957 (Kotlin inline search resolution indicator).
- nbd-wtf/nostr-tools PR #533 (Namecoin NIP-05 in nostr-tools, open).
- CodyTseng/jumble PR #774 (parallel TS port, open).
The resolver hits one of three public Namecoin ElectrumX endpoints
over WSS, falls through to the next on transport error, returns null
on a definitive name-not-found. TLSA pinning is intentionally out of
scope for this PR; the server-side API route is the recommended entry
point because the public ElectrumX operators currently serve
self-signed certs that browsers refuse.
|
@mstrofnone is attempting to deploy a commit to the dergigi's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR introduces Namecoin ChangesNamecoin NIP-05 Resolution and Integration
Sequence Diagram(s)sequenceDiagram
participant Caller
participant QueryProfile
participant nameShow
participant ElectrumX as ElectrumX WSS
participant ParseOps as Script Parser
Caller->>QueryProfile: identifier
QueryProfile->>nameShow: identifier, servers
nameShow->>ElectrumX: get_history(script_hash)
ElectrumX-->>nameShow: tx history
nameShow->>ParseOps: parse OP_NAME_UPDATE
ParseOps-->>nameShow: decoded value JSON
nameShow->>QueryProfile: { pubkey, relays }
QueryProfile-->>Caller: NamecoinResolveResult
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bring DEFAULT_ELECTRUMX_SERVERS to parity with amethyst's quartz/.../namecoin/ElectrumXServer.kt DEFAULT_ELECTRUMX_SERVERS, keeping only the 4 entries that publish a WSS endpoint reachable from a browser. The two bare-IP entries from amethyst's list (23.158.233.10, 46.229.238.187) are intentionally omitted: browsers refuse WSS to bare IPs without an IP-SAN cert, so they would just fail the handshake. Order matches amethyst: 1. electrumx.testls.space (primary) 2. nmc2.bitcoins.sk 3. relay.testls.bit (the only .bit-addressed entry) 4. electrum.nmc.ethicnology.com (Let's Encrypt; easiest from browser)
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/namecoin/nip05.ts (1)
1-593: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy liftSplit this module to satisfy the max file-length rule.
This new file exceeds the repository limit and should be split (e.g., transport, script parsing, and value extraction into separate modules).
As per coding guidelines, "Keep files below 420 lines".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/namecoin/nip05.ts` around lines 1 - 593, The file is too long — split it into focused modules: transport (WebSocket/RPC, buildWSSUrl, useWebSocketImplementation, ElectrumXServer, DEFAULT_ELECTRUMX_SERVERS, nameShow/nameShowWithFallback and nameShow’s dependency on RPC), script parsing (buildNameIndexScript, pushData, electrumScriptHash, extractNameValue, parseNameScript, readPushData, hexToBytes), and JSON/value extraction (extractNostrFromValue, extractFromDomainNamesObject, extractFromIdentityObject, extractRelays, extractImports, + HEX_PUBKEY_RE and NamecoinResolveResult/ParsedIdentifier types if needed). Move the related symbols into those files and replace local references with imports/exports (e.g., queryProfile -> imports nameShowWithFallback from transport and extractNostrFromValue from value-extraction module; nameShow imports buildNameIndexScript and electrumScriptHash from script-parsing module). Ensure types (ParsedIdentifier, ElectrumXServer, NamecoinResolveResult) are exported where shared, and preserve function/class names (RPC, buildNameIndexScript, parseNameScript, extractNostrFromValue, extractRelays, nameShowWithFallback) so callers can find them after splitting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/NamecoinResolutionIndicator.tsx`:
- Around line 81-83: In NamecoinResolutionIndicator, the effect cleanup only
sets cancelled but leaves the pending debounce timer to fire; update the
returned cleanup to also clear and null out debounceRef.current (and call
clearTimeout on it) so the timer won't run after unmount and you won't trigger
stale lookups — e.g., in the effect's return use
clearTimeout(debounceRef.current), set debounceRef.current = null, and keep
cancelled = true to fully cancel pending work.
In `@src/components/SearchView.tsx`:
- Line 43: The SearchView component is growing too large—extract the Namecoin
indicator wiring and the adjacent search-header UI into a new child component to
reduce size; create a new component (e.g., NamecoinHeader or
SearchHeaderWithNamecoin) that imports NamecoinResolutionIndicator and accepts
the necessary props or callbacks currently used in SearchView (state setters,
search term, resolution status, etc.), move the JSX and related handlers from
SearchView into that component, update SearchView to render the new child (pass
down props/functions), and remove the now-duplicated imports/logic so SearchView
shrinks below the 420-line guideline.
In `@src/lib/namecoin/index.ts`:
- Around line 75-79: The fallback path currently calls rawIsValid(pubkeyHex,
identifier) which ignores the provided servers; update the fallback to pass the
callers' servers (e.g., rawIsValid(pubkeyHex, identifier, servers)) and modify
rawIsValid's signature/implementation to accept and use the servers parameter
(propagating it to any internal resolver or HTTP client) so the same server list
is used on both resolveNamecoinNip05 and the raw verification fallback.
- Around line 42-46: The cache/inflight lookups for namecoin resolution (calls
to getCachedNamecoinResolve and getInflightNamecoinResolve using identifier)
ignore the `servers` option and can return results from a different server set;
update the keying logic so the cache/inflight keys incorporate a stable
fingerprint of the servers (e.g., JSON.stringify or a short hash of the
`servers` array) or skip cache/inflight when a custom `servers` value is
provided; specifically change the calls around
getCachedNamecoinResolve(identifier) and getInflightNamecoinResolve(identifier)
(and the analogous checks at the other occurrence around lines 60-61) to use a
composite key like `${identifier}|${serversKey}` or to bypass caching when
`servers` is explicitly passed.
In `@src/lib/namecoin/nip05.ts`:
- Around line 435-441: The hexToBytes function currently allows partial parses
because parseInt silently accepts invalid chars; update hexToBytes to validate
each 2-char hexPair before parsing (e.g., ensure each hexPair matches
/^[0-9a-fA-F]{2}$/) and only then call parseInt, throwing a clear Error('hex:
invalid byte') for any non-matching pair; also factor out related utilities from
the large src/lib/namecoin/nip05.ts into a new module (e.g., nip05HexUtils or
hex.ts) to reduce file length below the 420-line limit.
---
Outside diff comments:
In `@src/lib/namecoin/nip05.ts`:
- Around line 1-593: The file is too long — split it into focused modules:
transport (WebSocket/RPC, buildWSSUrl, useWebSocketImplementation,
ElectrumXServer, DEFAULT_ELECTRUMX_SERVERS, nameShow/nameShowWithFallback and
nameShow’s dependency on RPC), script parsing (buildNameIndexScript, pushData,
electrumScriptHash, extractNameValue, parseNameScript, readPushData,
hexToBytes), and JSON/value extraction (extractNostrFromValue,
extractFromDomainNamesObject, extractFromIdentityObject, extractRelays,
extractImports, + HEX_PUBKEY_RE and NamecoinResolveResult/ParsedIdentifier types
if needed). Move the related symbols into those files and replace local
references with imports/exports (e.g., queryProfile -> imports
nameShowWithFallback from transport and extractNostrFromValue from
value-extraction module; nameShow imports buildNameIndexScript and
electrumScriptHash from script-parsing module). Ensure types (ParsedIdentifier,
ElectrumXServer, NamecoinResolveResult) are exported where shared, and preserve
function/class names (RPC, buildNameIndexScript, parseNameScript,
extractNostrFromValue, extractRelays, nameShowWithFallback) so callers can find
them after splitting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 000a084d-659c-4237-ba72-faa32b44b577
📒 Files selected for processing (8)
src/app/api/nip05/verify/route.tssrc/components/NamecoinResolutionIndicator.tsxsrc/components/SearchView.tsxsrc/lib/namecoin/__tests__/nip05.test.tssrc/lib/namecoin/cache.tssrc/lib/namecoin/index.tssrc/lib/namecoin/nip05.tssrc/lib/profile/nip05.ts
| return () => { | ||
| cancelled = true; | ||
| }; |
There was a problem hiding this comment.
Clear debounce timer in effect cleanup to avoid stale post-unmount lookups.
Cleanup sets cancelled, but the timer still executes and can start a needless request. Also clear debounceRef.current in the returned cleanup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/NamecoinResolutionIndicator.tsx` around lines 81 - 83, In
NamecoinResolutionIndicator, the effect cleanup only sets cancelled but leaves
the pending debounce timer to fire; update the returned cleanup to also clear
and null out debounceRef.current (and call clearTimeout on it) so the timer
won't run after unmount and you won't trigger stale lookups — e.g., in the
effect's return use clearTimeout(debounceRef.current), set debounceRef.current =
null, and keep cancelled = true to fully cancel pending work.
| import VideoWithBlurhash from '@/components/VideoWithBlurhash'; | ||
| import SearchInput from '@/components/SearchInput'; | ||
| import QueryTranslation from '@/components/QueryTranslation'; | ||
| import NamecoinResolutionIndicator from '@/components/NamecoinResolutionIndicator'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Avoid adding new UI responsibilities to an already oversized file.
Please extract the Namecoin indicator wiring (and nearby search-header UI) into a smaller child module to move this file back toward the size constraint.
As per coding guidelines, "Keep files below 420 lines".
Also applies to: 1657-1657
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/SearchView.tsx` at line 43, The SearchView component is
growing too large—extract the Namecoin indicator wiring and the adjacent
search-header UI into a new child component to reduce size; create a new
component (e.g., NamecoinHeader or SearchHeaderWithNamecoin) that imports
NamecoinResolutionIndicator and accepts the necessary props or callbacks
currently used in SearchView (state setters, search term, resolution status,
etc.), move the JSX and related handlers from SearchView into that component,
update SearchView to render the new child (pass down props/functions), and
remove the now-duplicated imports/logic so SearchView shrinks below the 420-line
guideline.
| const cached = getCachedNamecoinResolve(identifier); | ||
| if (cached !== undefined) return cached; | ||
|
|
||
| const inflight = getInflightNamecoinResolve(identifier); | ||
| if (inflight) return inflight; |
There was a problem hiding this comment.
Cache/inflight keys ignore servers, causing cross-server result contamination.
Lookups with custom servers can return cached/inflight results produced by a different server set. Include a server fingerprint in keys (or bypass cache when servers is explicitly passed).
Also applies to: 60-61
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/namecoin/index.ts` around lines 42 - 46, The cache/inflight lookups
for namecoin resolution (calls to getCachedNamecoinResolve and
getInflightNamecoinResolve using identifier) ignore the `servers` option and can
return results from a different server set; update the keying logic so the
cache/inflight keys incorporate a stable fingerprint of the servers (e.g.,
JSON.stringify or a short hash of the `servers` array) or skip cache/inflight
when a custom `servers` value is provided; specifically change the calls around
getCachedNamecoinResolve(identifier) and getInflightNamecoinResolve(identifier)
(and the analogous checks at the other occurrence around lines 60-61) to use a
composite key like `${identifier}|${serversKey}` or to bypass caching when
`servers` is explicitly passed.
| const res = await resolveNamecoinNip05(identifier, servers); | ||
| if (res) return res.pubkey.toLowerCase() === pubkeyHex.toLowerCase(); | ||
| // Fall back to the raw path so callers can still verify with custom servers. | ||
| return rawIsValid(pubkeyHex, identifier); | ||
| } |
There was a problem hiding this comment.
Fallback verification ignores the caller’s servers argument.
When resolution misses, rawIsValid(pubkeyHex, identifier) uses default servers, not the provided servers, so behavior diverges from function inputs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/namecoin/index.ts` around lines 75 - 79, The fallback path currently
calls rawIsValid(pubkeyHex, identifier) which ignores the provided servers;
update the fallback to pass the callers' servers (e.g., rawIsValid(pubkeyHex,
identifier, servers)) and modify rawIsValid's signature/implementation to accept
and use the servers parameter (propagating it to any internal resolver or HTTP
client) so the same server list is used on both resolveNamecoinNip05 and the raw
verification fallback.
| function hexToBytes(hex: string): Uint8Array { | ||
| if (hex.length % 2 !== 0) throw new Error('hex: odd length'); | ||
| const out = new Uint8Array(hex.length / 2); | ||
| for (let i = 0; i < out.length; i++) { | ||
| const b = parseInt(hex.slice(i * 2, i * 2 + 2), 16); | ||
| if (Number.isNaN(b)) throw new Error('hex: invalid byte'); | ||
| out[i] = b; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In JavaScript, what does parseInt('0g', 16)return, and doesparseInt allow partial parsing for invalid trailing characters?
💡 Result:
parseInt('0g', 16) returns 0. Explanation: In radix 16, the valid digits are 0-9 and A-F (case-insensitive). When parseInt encounters a character that is not a valid numeral in the specified radix, it ignores that character and all succeeding characters, returning the integer value parsed up to that point [1][2]. For the string '0g' at radix 16, '0' is valid and 'g' is not; so it parses only the leading '0' and stops, yielding 0 [1][2]. Does parseInt allow partial parsing for invalid trailing characters? Yes. parseInt performs “prefix” parsing: it parses the initial sequence of valid digits for the given radix, and when it hits the first invalid trailing character, it stops parsing there rather than requiring the entire string to be valid [1][2]. If the first character cannot be converted to a number in the specified radix, then parseInt returns NaN instead [1][2].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt
- 2: https://udn.realityripple.com/docs/Web/JavaScript/Reference/Global_Objects/parseInt
🏁 Script executed:
wc -l src/lib/namecoin/nip05.tsRepository: dergigi/ants
Length of output: 86
🏁 Script executed:
sed -n '435,441p' src/lib/namecoin/nip05.tsRepository: dergigi/ants
Length of output: 285
🏁 Script executed:
wc -l src/lib/namecoin/nip05.ts && rg "function hexToBytes" src/lib/namecoin/nip05.ts -A 10Repository: dergigi/ants
Length of output: 441
Harden hex-byte validation to reject partial parses.
parseInt(hexPair, 16) performs prefix parsing and silently accepts partial input (e.g., '0g' parses to 0 instead of throwing). This allows malformed hex to be accepted instead of rejected.
Suggested fix
function hexToBytes(hex: string): Uint8Array {
if (hex.length % 2 !== 0) throw new Error('hex: odd length');
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) {
- const b = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
- if (Number.isNaN(b)) throw new Error('hex: invalid byte');
+ const pair = hex.slice(i * 2, i * 2 + 2);
+ if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('hex: invalid byte');
+ const b = Number.parseInt(pair, 16);
out[i] = b;
}
return out;
}Additionally, the file exceeds the 420-line limit (currently 601 lines). Consider splitting functionality into separate modules.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function hexToBytes(hex: string): Uint8Array { | |
| if (hex.length % 2 !== 0) throw new Error('hex: odd length'); | |
| const out = new Uint8Array(hex.length / 2); | |
| for (let i = 0; i < out.length; i++) { | |
| const b = parseInt(hex.slice(i * 2, i * 2 + 2), 16); | |
| if (Number.isNaN(b)) throw new Error('hex: invalid byte'); | |
| out[i] = b; | |
| function hexToBytes(hex: string): Uint8Array { | |
| if (hex.length % 2 !== 0) throw new Error('hex: odd length'); | |
| const out = new Uint8Array(hex.length / 2); | |
| for (let i = 0; i < out.length; i++) { | |
| const pair = hex.slice(i * 2, i * 2 + 2); | |
| if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('hex: invalid byte'); | |
| const b = Number.parseInt(pair, 16); | |
| out[i] = b; | |
| } | |
| return out; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/namecoin/nip05.ts` around lines 435 - 441, The hexToBytes function
currently allows partial parses because parseInt silently accepts invalid chars;
update hexToBytes to validate each 2-char hexPair before parsing (e.g., ensure
each hexPair matches /^[0-9a-fA-F]{2}$/) and only then call parseInt, throwing a
clear Error('hex: invalid byte') for any non-matching pair; also factor out
related utilities from the large src/lib/namecoin/nip05.ts into a new module
(e.g., nip05HexUtils or hex.ts) to reduce file length below the 420-line limit.
Summary
Adds Namecoin
.bitresolution to the existing NIP-05 path, plus an inline resolution indicator under the search bar so users can paste a.bitidentifier and see who it resolves to without leaving the page.Supported identifier shapes:
alice@example.bitexample.bit(uses the_root entry)d/example(Namecoin domain namespace)id/alice(Namecoin identity namespace)nostr:NIP-21 prefix tolerated on all of the aboveWhy
NIP-05 only ever defined the wire format, never bound the lookup transport to HTTPS — most clients just default there because that is the path the spec sketches. Namecoin
.bitnames ship the exact samenostr.names[<localpart>]/nostr.pubkeyJSON shape, just stored on-chain instead of in/.well-known/nostr.json. A.bitidentifier survives DNS revocation and gives the user a self-custodial identifier with the same UX as a NIP-05.ants is a search client first — being able to type
m@testls.bitinto the search bar and see it resolve to a real npub (with a clear failure mode when it does not) is the cheapest possible demonstration that the two transports can coexist.What it does
src/lib/namecoin/nip05.ts.bitresolver: WSS to an ElectrumX server, scripthash path viablockchain.scripthash.get_history, NAME_UPDATE vout parsing, name-expiry check (~36 000 blocks), JSON extraction for bothd/(names map + relays) andid/(identity object) shapes. Pluggable WebSocket impl viauseWebSocketImplementation.src/lib/namecoin/cache.tssrc/lib/namecoin/index.tsresolveNamecoinNip05andisValidNamecoinNip05.src/lib/profile/nip05.ts.bitidentifiers through the chain resolver from bothresolveNip05ToPubkeyandverifyNip05, so any existing call site that already handles a NIP-05 value picks.bitup for free.src/app/api/nip05/verify/route.ts.bitidentifiers throughisValidNamecoinNip05on the server side so the browser never has to deal with the self-signed ElectrumX TLS certs the public servers ship today.src/components/NamecoinResolutionIndicator.tsxnpubshort-form +Namecoinbadge (click into/p/<npub>) → explicit failure message on lookup error or missingnostrfield.src/components/SearchView.tsx<SearchInput>and<QueryTranslation>. Renders nothing when the query is not a.bitidentifier.Wire-format references
This is a TypeScript port of the Namecoin NIP-05 wire format that already ships in multiple other clients. All five implementations interop on the same on-chain JSON shape:
vitorpamplona/amethyst(PRs #1771, #2707, #2747, #2801, #2911, #2956, #2957)nostur-com/nostur-ios-publicPR Highlight zap senders in profile lightning UI #60nbd-wtf/nostr-toolsPR #533 (open) andCodyTseng/jumblePR #774 (open)ethicnology/dart-nostrPR Improve mobile UX and add search examples #44 (merged)Amethyst PR #2957 is the direct precedent for the search-bar indicator: a small inline row mounted directly under the search input, showing spinner →
Namecoinbadge + resolved user, or an explicit failure variant. This PR ports that pattern to ants' web search bar.The on-chain format itself is documented in the draft Namecoin NIP-05 track (https://github.com/mstrofnone/nips —
N1.md), which has been cross-posted to nostr-protocol/nips for discussion.Why not depend on nostr-tools directly
nbd-wtf/nostr-toolsPR #533 adds anip05namecoinexport with the exact same API surface, but is still open. Rather than gate this PR on it landing, the resolver is shipped inline insrc/lib/namecoin/— mirroring the path Jumble PR #774 took. If/when #533 ships, switching this PR's internals over is a ~50-line swap.Default ElectrumX server set
DEFAULT_ELECTRUMX_SERVERSmirrors amethyst'squartz/.../namecoin/ElectrumXServer.ktDEFAULT_ELECTRUMX_SERVERS(4-of-6 subset). Amethyst additionally ships two bare-IP entries (23.158.233.10,46.229.238.187) for the JVM TLS path, but browsers refuse WSS to bare IPs without an IP-SAN cert, so we intentionally omit them here:The last entry ships a Let's Encrypt cert, which makes it the easiest one to reach from a browser today. The first three serve self-signed certs (see TLSA scope below).
TLSA / browser TLS scope
The public Namecoin ElectrumX operators currently serve self-signed certificates, so direct browser WSS dials will fail TLS. The verification flow goes through
/api/nip05/verifyon the Next.js server side, where Node's TLS stack can accept the self-signed CA via the pinned-cert dance (we leave that to the deployer's environment, no extra dependency required for the lookup itself).TLSA pinning of the ElectrumX dial is intentionally out of scope for this PR; it is tracked separately in the Namecoin NIP-05 track as
N3.Verification
The 19 new tests cover identifier parsing (
.bit/d//id/,nostr:prefix,user@domain.bit), JSON extraction (simple form, extended names map,id/identity object, malformed input), relay surfacing, and theimport-walking helper.Out of scope
N3) — separate PR.wss://example.bit(N2) — separate PR..bitpayment pointers / CLINK extension (N5) — separate PR.