Skip to content

Add .bit nip-05 resolver via ElectrumX (namecoin)#9

Open
mstrofnone wants to merge 2 commits into
eskema:mainfrom
mstrofnone:feat/namecoin-bit-nip05
Open

Add .bit nip-05 resolver via ElectrumX (namecoin)#9
mstrofnone wants to merge 2 commits into
eskema:mainfrom
mstrofnone:feat/namecoin-bit-nip05

Conversation

@mstrofnone
Copy link
Copy Markdown

Resolve .bit NIP-05 identifiers via Namecoin

Adds a small .bit-suffix branch to alphaama's existing NIP-05 verification
path. Identifiers ending in .bit (e.g. m@something.bit, or bare
something.bit) get resolved against Namecoin via ElectrumX over WSS;
everything else still goes through NostrTools.nip05.queryProfile exactly
as today.

Why

Namecoin lets people anchor a Nostr identity to a name they actually own on
chain instead of one rented from a webserver. The wire format is small,
self-contained, and already in use by other Nostr clients (Amethyst,
Nostur, Jumble, nostrmo, and others — see references below). Wiring it
into alphaama is ~165 lines of vanilla JS, matches the scrappy style of
the codebase, and doesn't touch any existing call path that isn't a
.bit address.

How

Single new file: aa/fx/namecoin.js

  • aa.fx.is_bit_nip05(s) — cheap suffix check, callers branch on it
  • aa.fx.nip05_namecoin(s) — drop-in for nip05.queryProfile(s),
    returns {pubkey, relays} or null
  • aa.fx.namecoin_resolve(key) — calls blockchain.name.show over wss,
    per-page TTL cache (5 min)
  • aa.fx.electrumx_name_show(url, name) — one-shot wss JSON-RPC

Two hook points (one line each, default path unchanged):

  • p/p.js aa.p.check_nip05 — backs the .p check <name@domain> CLI
    command and the click handler on the profile UI's NIP-05 row
  • u/u.js setup flow — backs login by NIP-05 identifier

Server set

Browser-WSS subset of the canonical six ElectrumX servers, mirroring
Amethyst's DEFAULT_ELECTRUMX_SERVERS:

host wss port
electrumx.testls.space 50004
nmc2.bitcoins.sk 57004
relay.testls.bit 50004
electrum.nmc.ethicnology.com 50004

Tried in order, first non-null wins, 5-minute cache. Bare-IP entries from
the canonical set are skipped because browser WSS needs SAN-matched certs
— they'd need TLSA TOFU pinning (separate NIP-track work) to be usable
from a browser.

Wire format

Covers the three nostr value shapes specified by ifa-0001 Domain Name
Object (already deployed on-chain by multiple identities today):

// shape 1: bare pubkey
{"nostr": "<hex64>"}

// shape 2: single id with optional relays
{"nostr": {"pubkey": "<hex64>", "relays": ["wss://...", ...]}}

// shape 3: names directory
{"nostr": {"names": {"alice": "<hex64>", ...}, "relays": [...]}}

Plus shallow one-hop import walking (keeps the implementation tight —
deep walking is unnecessary for the common case) and map sub-label
descent for subdomain.<name>.bit style identifiers. Tries d/<name>
first, then id/<name>.

Style notes

This matches alphaama's scrappy style — minimum viable resolver, ~165
lines incl. comments, single file, hangs off the global aa.* namespace,
snake_case, Allman braces, no build step, no module dance. Could have
been a n/ module with a mk.js and a CLI command set, but that would
be three times the code for the same behaviour. Happy to expand if you'd
rather have it as a proper module.

Verifying

.p check mstrofnone.bit

(any .bit name with a nostr field will work — current testers include
mstrofnone.bit, testls.bit, and several others on the chain today).

Diff

+219 / -3 across 4 files:

  • aa/fx/namecoin.js (new, +219)
  • aa/aa.js (+1 — adds the new fx file to the load list)
  • p/p.js (+4, -2 — .bit branch in check_nip05)
  • u/u.js (+2, -1 — .bit branch in setup)

References (cross-client wire-format precedent)

mstrofnone added 2 commits May 19, 2026 23:46
resolves <local>@<host>.bit (or bare <host>.bit) to nostr pubkey + relays
via Namecoin ElectrumX servers (browser-WSS). hooks into the two existing
nip-05 entry points:

  - aa.p.check_nip05 (.p check command + profile UI button)
  - aa.u.setup_sheet flow (login via nip-05 identifier)

both fall through to NostrTools.nip05.queryProfile for non-.bit inputs.

implementation:
  aa/fx/namecoin.js — single file, ~165 lines incl. comments
    - aa.fx.is_bit_nip05(s) — cheap suffix check
    - aa.fx.nip05_namecoin(s) — drop-in for nip05.queryProfile
    - aa.fx.namecoin_resolve(key) — name_show with per-page TTL cache
    - aa.fx.electrumx_name_show(url, name) — one-shot wss JSON-RPC

server set mirrors amethyst's DEFAULT_ELECTRUMX_SERVERS (browser-WSS subset,
4 of 6 — bare-IP entries skipped because WSS to bare IPs needs IP-SAN certs).
servers are tried in order, first non-null answer wins, results cached for
5 minutes.

wire format covers all three ifa-0001 `nostr` shapes:
  - plain hex pubkey
  - {pubkey, relays}
  - {names: {<local>: <pubkey>}, relays?}
plus shallow one-hop `import` walking and `map` sub-label descent.

matches alphaama's existing style: vanilla JS, snake_case, Allman braces,
hangs off the global `aa.*` namespace, no build step.
The WSS receive loop in aa.fx.electrumx_name_show consumed the FIRST
inbound frame unconditionally. ElectrumX is free to push unsolicited
notifications (server.banner on connect, blockchain.headers.subscribe
deltas) BEFORE the actual response to our request, and JSON-RPC is
fundamentally bidirectional — those pushes carry a 'method' field and
no matching 'id'. When one of them arrived first, the resolver flipped
its done flag, closed the socket, and resolved with whatever .result
was on the notification (usually undefined, surfacing as null).

Net effect: a name that *exists* on Namecoin would intermittently
resolve to null, the resolver would walk to the next server, and on
unlucky days every server would push first and the name appeared
unresolvable. The 5 min positive-TTL cache then locked that null in.

Fix:
  - Generate a unique outgoing id per call and capture it in the closure.
  - In onmessage, ignore any frame that:
      * fails to parse (keep listening until timeout)
      * has a 'method' field (server-initiated notification)
      * has an id that doesn't match the outgoing request
    Only a frame with the matching id flips done and resolves/rejects.
  - The existing timeout still rejects if no matching frame ever lands.

Also: split the namecoin_cache TTL so a null result lives only 30s while
positive results keep the existing 5 min window. Prevents one transient
miss from poisoning a name for the full positive TTL.

Tests: aa/fx/namecoin.test.html / .test.js — open in a browser, or run
via a tiny Node vm shim. The harness mocks WebSocket and replays
server.banner + headers.subscribe pushes before the real response,
asserting the resolver returns the parsed value not null. Also covers
stray frames with missing id, mismatched id, rpc-error frames,
push-only timeouts, and the negative-cache TTL split.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant