Two more adapters under Settings › Names & Certificates:
Namecoin — .bit names resolve by calling name_show "d/<name>" on the
configured JSON-RPC endpoint and following `translate` or `alias` records
to a clearnet host. Pure IP records aren't followed yet (that needs
in-browser DNS override + TLS pinning, which comes later). Namecoin has
no free public RPC, so the toggle is off by default and the row carries a
"Needs an RPC URL" note instead of a "Not wired yet" one; set yours in
Advanced.
Handshake — non-ICANN hostnames resolve through the public hns.to web
gateway (user-overridable in Advanced). hns.to proxies the request to a
Handshake node and serves the on-chain content. A bundled list of common
ICANN TLDs (original gTLDs + popular new gTLDs + every ccTLD) keeps the
default collision policy ("icann-wins") from hijacking clearnet names.
Setting the policy to "hns-wins" removes that gate.
Both adapters follow the ENS adapter's shape — tryExtraResolvers in main.js
calls each in turn after the BNS lookup; the first one that returns a URL
loads it. Settings copy on both rows updated.
99 lines
4.1 KiB
JavaScript
99 lines
4.1 KiB
JavaScript
"use strict";
|
||
// Namecoin resolver — .bit names via a configured Namecoin JSON-RPC endpoint.
|
||
// Reads the Namecoin domain record format (dot-bit spec) and follows an
|
||
// `alias` or `translate` field to a clearnet host when present. Pure IP
|
||
// records aren't followed yet — rendering those cleanly requires in-browser
|
||
// DNS override + TLS pinning, which lives in a later phase.
|
||
//
|
||
// Namecoin has no public free RPC (unlike publicnode for Ethereum), so the
|
||
// user must configure their own RPC URL in Settings › Names & Certificates
|
||
// › Namecoin › Advanced. Common sources: a self-run namecoind, Zeronet's
|
||
// name-lookup gateway, or a shared RPC from the Namecoin community.
|
||
//
|
||
// Namecoin record format (JSON, from name_show "d/<name>"):
|
||
// {"ip":"192.0.2.1", "ip6":"...", "translate":"example.com",
|
||
// "alias":"other.bit", "map":{"www":{"ip":"..."}}, ...}
|
||
// Spec: https://wiki.namecoin.org/index.php?title=Domain_Name_Specification_2.0
|
||
|
||
const POS_TTL_MS = 10 * 60 * 1000;
|
||
const NEG_TTL_MS = 60 * 1000;
|
||
const RPC_TIMEOUT_MS = 5000;
|
||
const CACHE = new Map();
|
||
|
||
async function rpcCall(rpc, method, params) {
|
||
const body = { jsonrpc: "1.0", id: "theseus", method, params };
|
||
const ctrl = new AbortController();
|
||
const t = setTimeout(() => ctrl.abort(new Error("Namecoin RPC timeout")), RPC_TIMEOUT_MS);
|
||
try {
|
||
const res = await fetch(rpc, {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
signal: ctrl.signal,
|
||
});
|
||
if (!res.ok && res.status !== 500) throw new Error(`Namecoin RPC HTTP ${res.status}`);
|
||
const json = await res.json();
|
||
if (json.error) throw new Error(`Namecoin RPC error: ${json.error.message || JSON.stringify(json.error)}`);
|
||
return json.result;
|
||
} finally { clearTimeout(t); }
|
||
}
|
||
|
||
// Follow one hop of a dot-bit `alias` chain. `alias` points to another
|
||
// Namecoin name (like `example.bit`); `translate` points to a clearnet host.
|
||
// We walk at most 5 aliases to avoid loops.
|
||
async function readRecord(rpc, name, hops = 0) {
|
||
if (hops > 5) throw new Error("alias loop");
|
||
const key = `d/${name.replace(/\.bit$/i, "")}`;
|
||
const row = await rpcCall(rpc, "name_show", [key]);
|
||
if (!row || !row.value) throw new Error("name not registered");
|
||
let rec; try { rec = JSON.parse(row.value); } catch { throw new Error("record is not JSON"); }
|
||
if (rec.alias && typeof rec.alias === "string") {
|
||
// alias is another .bit name
|
||
const next = rec.alias.replace(/\.bit$/i, "");
|
||
return readRecord(rpc, next, hops + 1);
|
||
}
|
||
return rec;
|
||
}
|
||
|
||
async function resolve(host, { rpc } = {}) {
|
||
if (!host) return null;
|
||
const name = String(host).toLowerCase();
|
||
if (!name.endsWith(".bit")) return null;
|
||
const cached = CACHE.get(name);
|
||
if (cached && cached.exp > Date.now()) return cached.result;
|
||
|
||
if (!rpc) {
|
||
const result = { error: "no RPC URL configured (Settings › Names & Certificates › Namecoin › Advanced)" };
|
||
CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS });
|
||
return result;
|
||
}
|
||
|
||
try {
|
||
const rec = await readRecord(rpc, name);
|
||
// Prefer translate (clearnet redirect) → navigate there.
|
||
if (rec.translate && typeof rec.translate === "string") {
|
||
const dest = rec.translate.replace(/^https?:\/\//, "");
|
||
const url = `https://${dest}`;
|
||
const result = { url, protocol: "namecoin", kind: "translate", target: dest };
|
||
CACHE.set(name, { result, exp: Date.now() + POS_TTL_MS });
|
||
return result;
|
||
}
|
||
// Pure IP records: not followed (needs host-rules + cert handling).
|
||
if (rec.ip || rec.ip6) {
|
||
const result = { error: "pure IP records not supported yet; add a translate= field or wait for IP-record support" };
|
||
CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS });
|
||
return result;
|
||
}
|
||
const result = { error: "no translate/alias/ip record" };
|
||
CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS });
|
||
return result;
|
||
} catch (err) {
|
||
const result = { error: String(err?.message || err) };
|
||
CACHE.set(name, { result, exp: Date.now() + NEG_TTL_MS });
|
||
return result;
|
||
}
|
||
}
|
||
|
||
function clearCache() { CACHE.clear(); }
|
||
|
||
module.exports = { resolve, clearCache };
|