bns: one network table instead of chipnet constants copied into fifteen places
Every client hard-coded the chipnet beacons, address prefix and electrum servers on its own: resolver, registrar config, wallets, gateway, indexer, mirror scripts, the browser bundle and the mobile Java. A mainnet launch would have meant finding all of them and hoping none was missed. The table now lives in resolver-web.js, the one file every client already shares, so it stays a single-file drop-in. BNS_NETWORK selects the record; unset means chipnet, so nothing changes today: the live index resolves the same 60 names and 20 TLDs, the 67 offline tests pass, and the dashboard, market and studio load the same values through BNS.NETWORK. The mainnet record carries the verified public servers, the prefix and its own Sia bucket, but its beacons, start height and operator address are deliberately null: requireBeacons() refuses to scan until they are pinned in the order ROADMAP-MAINNET.md §6 requires. Bns.java reads a generated BnsNetwork.java so the phone cannot drift from the desktop clients. NETWORK-CONFIG.md records what reads the table and what a launch still pins.
This commit is contained in:
parent
6846d8dde9
commit
53428a605f
24 changed files with 382 additions and 181 deletions
|
|
@ -1,9 +1,8 @@
|
|||
export const BEACON = "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl";
|
||||
// TLD-certificate beacon (Decentralized.DNS/DESIGN-tld-registry.md §3).
|
||||
// Distinct from the name BEACON so a client that only wants the TLD list can
|
||||
// walk one address instead of every name-registration transaction. Chipnet.
|
||||
// Scripthash (electrum encoding): c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8
|
||||
export const TLD_BEACON = "bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh";
|
||||
import { NETWORK } from "./lib/resolver-web.js";
|
||||
// Beacons of the active network (BNS_NETWORK; chipnet unless set). The table
|
||||
// itself lives in lib/resolver-web.js — see the note there.
|
||||
export const BEACON = NETWORK.beacon;
|
||||
export const TLD_BEACON = NETWORK.tldBeacon;
|
||||
export const PREFIX = "BNS1";
|
||||
export const TLD = ".bch";
|
||||
export const BEACON_DUST_SAT = 600;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,16 @@ import WebSocket from "ws";
|
|||
// resolver-web.js lives at ../lib/, not alongside this file — the previous
|
||||
// `./resolver-web.js` import was broken (file was moved into lib/ but this
|
||||
// import wasn't updated). Fix while touching the file.
|
||||
import { resolveName, buildIndex, normalizeName, connectAny, CHIPNET_ELECTRUM, TLD_BEACON_SCRIPTHASH } from "./resolver-web.js";
|
||||
import { resolveName, buildIndex, normalizeName, connectAny, NETWORK, requireBeacons } from "./resolver-web.js";
|
||||
// Everything chain-specific comes from the network table (BNS_NETWORK env;
|
||||
// chipnet unless set). Refuse to start on a network whose beacons are unpinned.
|
||||
requireBeacons(NETWORK);
|
||||
const TLD_BEACON_SCRIPTHASH = NETWORK.tldBeaconScripthash;
|
||||
// Public electrum servers that answer wallet queries (listunspent). Our own
|
||||
// beacon-only indexers are filtered out by the table's beaconOnly pattern.
|
||||
const FULL_ELECTRUM = NETWORK.electrum.filter((s) => !NETWORK.beaconOnly.test(s.url));
|
||||
// Sia bucket holding operator-provided hosting: <bucket>/<name>/…
|
||||
const HOSTING_BUCKET = NETWORK.hostingBucket;
|
||||
import { pickRecord, isSubdomainOf } from "./record-picker.js";
|
||||
import { buildTldSnapshot } from "./tld-index.js";
|
||||
import { verifyListing } from "./market-tx.js";
|
||||
|
|
@ -43,10 +52,7 @@ import { readFileSync as readFileSyncOracle } from "node:fs";
|
|||
let _tldSnap = { at: 0, snap: null };
|
||||
async function getTldSnapshot() {
|
||||
if (_tldSnap.snap && Date.now() - _tldSnap.at < 30000) return _tldSnap.snap;
|
||||
const BNS_INDEXER_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/;
|
||||
const candidates = CHIPNET_ELECTRUM
|
||||
.map((s) => (typeof s === "string" ? { url: s } : s))
|
||||
.filter((s) => !BNS_INDEXER_URLS.test(s.url));
|
||||
const candidates = FULL_ELECTRUM;
|
||||
let el;
|
||||
for (const s of candidates) {
|
||||
try { el = await connectAny([s], WebSocket); break; } catch { /* try next */ }
|
||||
|
|
@ -131,7 +137,7 @@ function recoverSigner(sigB64, msg, likeAddress) {
|
|||
const digest = sha256.hash(utf8ToBin(msg));
|
||||
const pub = secp256k1.recoverPublicKeyCompressed(sigBytes.slice(1), (sigBytes[0] - 27) & 3, digest);
|
||||
if (typeof pub === "string") return null;
|
||||
const prefix = String(likeAddress || "bchtest:").split(":")[0] || "bchtest";
|
||||
const prefix = String(likeAddress || NETWORK.prefix + ":").split(":")[0] || NETWORK.prefix;
|
||||
const enc = encodeCashAddress({ prefix, type: "p2pkh", payload: ripemd160.hash(sha256.hash(pub)) });
|
||||
return typeof enc === "string" ? enc : (enc?.address ?? null);
|
||||
} catch { return null; }
|
||||
|
|
@ -139,8 +145,7 @@ function recoverSigner(sigB64, msg, likeAddress) {
|
|||
|
||||
// One full-electrum call (beacon-only indexers don't serve listunspent).
|
||||
async function withFullElectrum(fn) {
|
||||
const BNS_INDEXER_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/;
|
||||
const candidates = CHIPNET_ELECTRUM.map((s) => (typeof s === "string" ? { url: s } : s)).filter((s) => !BNS_INDEXER_URLS.test(s.url));
|
||||
const candidates = FULL_ELECTRUM;
|
||||
let el = null, lastErr = null;
|
||||
for (const s of candidates) { try { el = await connectAny([s], WebSocket); break; } catch (e) { lastErr = e; } }
|
||||
if (!el) throw new Error("no full electrum reachable: " + String(lastErr?.message || lastErr));
|
||||
|
|
@ -987,12 +992,8 @@ const server = http.createServer(async (req, res) => {
|
|||
if (scripthashes.length > 64) {
|
||||
res.writeHead(400); res.end(JSON.stringify({ error: "at most 64 scripthashes per request" })); return;
|
||||
}
|
||||
// Skip beacon-only indexers (silentmode.st/electrum on 443, our own
|
||||
// bns-indexer on :50011) — they don't implement listunspent.
|
||||
const BNS_INDEXER_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/;
|
||||
const candidates = CHIPNET_ELECTRUM
|
||||
.map((s) => (typeof s === "string" ? { url: s } : s))
|
||||
.filter((s) => !BNS_INDEXER_URLS.test(s.url));
|
||||
// Skip beacon-only indexers — they don't implement listunspent.
|
||||
const candidates = FULL_ELECTRUM;
|
||||
let el, lastErr;
|
||||
for (const s of candidates) {
|
||||
try { el = await connectAny([s], WebSocket); break; }
|
||||
|
|
@ -1218,7 +1219,7 @@ const server = http.createServer(async (req, res) => {
|
|||
// prefix as the on-chain owner address (chipnet = "bchtest",
|
||||
// mainnet = "bitcoincash"). The prefix comes from the owner
|
||||
// string itself so we don't have to hardcode chain.
|
||||
const ownerPrefix = String(entry.owner).split(":")[0] || "bchtest";
|
||||
const ownerPrefix = String(entry.owner).split(":")[0] || NETWORK.prefix;
|
||||
const pubkeyHash = ripemd160.hash(sha256.hash(recovered));
|
||||
const _enc = encodeCashAddress({ prefix: ownerPrefix, type: "p2pkh", payload: pubkeyHash });
|
||||
// libauth returns { address } in newer versions and a bare string in
|
||||
|
|
@ -1487,7 +1488,7 @@ const server = http.createServer(async (req, res) => {
|
|||
catch (e) { res.setHeader("content-type", "application/json"); res.writeHead(400); res.end(JSON.stringify({ error: e.message })); return; }
|
||||
const entry = (await getIndex()).get(name);
|
||||
if (!entry) { res.setHeader("content-type", "application/json"); res.writeHead(404); res.end(JSON.stringify({ error: `"${name}" not registered` })); return; }
|
||||
const prefix = `bns/${name}/`;
|
||||
const prefix = `${HOSTING_BUCKET}/${name}/`;
|
||||
// GET ...?src=live — a READ-ONLY view of wherever the name's on-chain
|
||||
// records say the site lives right now: a CLI-published bucket such as
|
||||
// bns/<label>/ (the `s3` record) or inline HTML (the `h` record). Sirius
|
||||
|
|
@ -1495,7 +1496,7 @@ const server = http.createServer(async (req, res) => {
|
|||
// they stay confined to bns/<name>/.
|
||||
const srcLive = req.method === "GET" && new URL(req.url, "http://x").searchParams.get("src") === "live";
|
||||
const recS3raw = typeof entry.records?.s3 === "string" ? entry.records.s3.trim().replace(/^\/+/, "").replace(/\/?$/, "/") : "";
|
||||
const livePrefix = srcLive && /^bns\/[A-Za-z0-9._\-\/]{1,200}$/.test(recS3raw) && !recS3raw.includes("..") ? recS3raw : null;
|
||||
const livePrefix = srcLive && new RegExp("^" + HOSTING_BUCKET.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\/[A-Za-z0-9._\\-\\/]{1,200}$").test(recS3raw) && !recS3raw.includes("..") ? recS3raw : null;
|
||||
const liveInline = srcLive && !livePrefix && typeof entry.records?.h === "string" && entry.records.h ? entry.records.h : null;
|
||||
const SITE_TYPES = { html: "text/html; charset=utf-8", htm: "text/html; charset=utf-8", css: "text/css; charset=utf-8", js: "text/javascript; charset=utf-8", mjs: "text/javascript; charset=utf-8", json: "application/json", txt: "text/plain; charset=utf-8", md: "text/markdown; charset=utf-8", xml: "application/xml", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", avif: "image/avif", ico: "image/x-icon", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", pdf: "application/pdf", mp3: "audio/mpeg", mp4: "video/mp4", webm: "video/webm", wasm: "application/wasm" };
|
||||
const typeOf = (p) => SITE_TYPES[(p.split(".").pop() || "").toLowerCase()] || "application/octet-stream";
|
||||
|
|
@ -1509,9 +1510,9 @@ const server = http.createServer(async (req, res) => {
|
|||
res.end(JSON.stringify({ name, prefix: null, source: liveInline ? "inline" : "none", files: liveInline ? [{ path: "index.html", size: Buffer.byteLength(liveInline), modified: null, inline: true }] : [] }));
|
||||
return;
|
||||
}
|
||||
const listPrefix = (srcLive ? livePrefix : prefix).slice("bns/".length);
|
||||
const listPrefix = (srcLive ? livePrefix : prefix).slice(HOSTING_BUCKET.length + 1);
|
||||
try {
|
||||
const up = await aws.fetch(`${S3_ENDPOINT}/bns?list-type=2&prefix=${encodeURIComponent(listPrefix)}&max-keys=1000`);
|
||||
const up = await aws.fetch(`${S3_ENDPOINT}/${HOSTING_BUCKET}?list-type=2&prefix=${encodeURIComponent(listPrefix)}&max-keys=1000`);
|
||||
if (!up.ok) { res.writeHead(502); res.end(JSON.stringify({ error: `sia list failed: ${up.status}` })); return; }
|
||||
const xml = await up.text();
|
||||
const files = [];
|
||||
|
|
@ -1571,7 +1572,7 @@ const server = http.createServer(async (req, res) => {
|
|||
const digest = sha256.hash(utf8ToBin(`BNS-SITE1\n${name}\n${filePath}\n${bodyHex}\n${ts}`));
|
||||
const recovered = secp256k1.recoverPublicKeyCompressed(sigBytes.slice(1), (sigBytes[0] - 27) & 3, digest);
|
||||
if (typeof recovered === "string") { res.writeHead(401); res.end(JSON.stringify({ error: `signature recovery failed: ${recovered}` })); return; }
|
||||
const ownerPrefix = String(entry.owner).split(":")[0] || "bchtest";
|
||||
const ownerPrefix = String(entry.owner).split(":")[0] || NETWORK.prefix;
|
||||
const encAddr = encodeCashAddress({ prefix: ownerPrefix, type: "p2pkh", payload: ripemd160.hash(sha256.hash(recovered)) });
|
||||
const derivedAddr = typeof encAddr === "string" ? encAddr : (encAddr?.address ?? "");
|
||||
if (derivedAddr !== entry.owner) { res.writeHead(403); res.end(JSON.stringify({ error: "signature does not match current on-chain NFT owner", derived: derivedAddr, current: entry.owner })); return; }
|
||||
|
|
@ -1804,7 +1805,7 @@ const server = http.createServer(async (req, res) => {
|
|||
const compactSig = sigBytes.slice(1);
|
||||
const recovered = secp256k1.recoverPublicKeyCompressed(compactSig, recovery, digest);
|
||||
if (typeof recovered === "string") { res.writeHead(502); res.end(JSON.stringify({ error: `sig recovery failed: ${recovered}` })); return; }
|
||||
const ownerPrefix = String(entry.owner).split(":")[0] || "bchtest";
|
||||
const ownerPrefix = String(entry.owner).split(":")[0] || NETWORK.prefix;
|
||||
const pubkeyHash = ripemd160.hash(sha256.hash(recovered));
|
||||
const _enc = encodeCashAddress({ prefix: ownerPrefix, type: "p2pkh", payload: pubkeyHash });
|
||||
const derivedAddr = typeof _enc === "string" ? _enc : (_enc?.address ?? "");
|
||||
|
|
|
|||
61
src/generate-network-constants.mjs
Normal file
61
src/generate-network-constants.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Generate the mobile resolver's network constants from the ONE network table
|
||||
// in src/lib/resolver-web.js, so Bns.java can never drift from the JS clients.
|
||||
//
|
||||
// node src/generate-network-constants.mjs [--network chipnet|mainnet] [outfile]
|
||||
//
|
||||
// Default outfile: AriadneResolver/mobile/app/src/com/silentmode/ariadne/BnsNetwork.java
|
||||
// The Java side dials raw-TLS electrum on port 50002 for public Fulcrums (and
|
||||
// wss for our own indexers), so the server list is re-expressed in that form.
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import { NETWORKS, DEFAULT_NETWORK_ID } from "./lib/resolver-web.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const netArg = args.includes("--network") ? args[args.indexOf("--network") + 1] : DEFAULT_NETWORK_ID;
|
||||
const net = NETWORKS[netArg];
|
||||
if (!net) { console.error(`unknown network ${netArg}`); process.exit(1); }
|
||||
const OUT = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--network")[0] || fileURLToPath(
|
||||
new URL("../../AriadneResolver/mobile/app/src/com/silentmode/ariadne/BnsNetwork.java", import.meta.url),
|
||||
);
|
||||
|
||||
// wss:// entries (our indexers) stay as they are, with the pinned ip; public
|
||||
// Fulcrums are dialled over raw TLS on the conventional 50002 port.
|
||||
const javaServer = (s) => {
|
||||
const u = new URL(s.url);
|
||||
const isIndexer = net.beaconOnly.test(s.url);
|
||||
const token = isIndexer ? `${s.url}${s.ip ? "|" + s.ip : ""}` : `${u.hostname}:${u.port === "50004" ? "50002" : u.port}`;
|
||||
return ` "${token}",`;
|
||||
};
|
||||
const q = (v) => (v == null ? "null" : JSON.stringify(v));
|
||||
|
||||
const java = `package com.silentmode.ariadne;
|
||||
|
||||
// GENERATED by Argus/src/generate-network-constants.mjs from the network table
|
||||
// in Argus/src/lib/resolver-web.js — do not edit; regenerate.
|
||||
// node src/generate-network-constants.mjs --network ${net.id}
|
||||
public final class BnsNetwork {
|
||||
private BnsNetwork() {}
|
||||
|
||||
public static final String ID = ${q(net.id)};
|
||||
public static final String LABEL = ${q(net.label)};
|
||||
public static final String ADDRESS_PREFIX = ${q(net.prefix)};
|
||||
// electrum scripthash of the name beacon (null while unpinned)
|
||||
public static final String BEACON_SCRIPTHASH = ${q(net.beaconScripthash)};
|
||||
// electrum scripthash of the TLD-certificate beacon
|
||||
public static final String TLD_BEACON_SCRIPTHASH = ${q(net.tldBeaconScripthash)};
|
||||
// Our own indexers first ("wss://host[|ip]"), then public electrum as
|
||||
// "host:port" (raw TLS). Same token format as the on-chain \`el\` record.
|
||||
public static final String[] SERVERS = {
|
||||
${net.electrum.map(javaServer).join("\n")}
|
||||
};
|
||||
public static final String ELECTRUM_LIST_NAME = ${q(net.electrumListName)};
|
||||
public static final String HOSTING_BUCKET = ${q(net.hostingBucket)};
|
||||
public static final String NAME_LIST_URL = ${q(net.nameListUrl)};
|
||||
public static final String CDN_INDEX_URL = ${q(net.cdnIndexUrl)};
|
||||
public static final String EXPLORER_TX = ${q(net.explorerTx)};
|
||||
}
|
||||
`;
|
||||
mkdirSync(path.dirname(OUT), { recursive: true });
|
||||
writeFileSync(OUT, java);
|
||||
console.log(`wrote ${OUT} for ${net.id} (${net.electrum.length} servers)`);
|
||||
|
|
@ -22,30 +22,28 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|||
import { pathToFileURL } from "node:url";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { buildTldSnapshot } from "../lib/tld-index.js";
|
||||
import { NETWORK } from "../lib/resolver-web.js";
|
||||
|
||||
// ---- config (env, with sane chipnet defaults) ----
|
||||
// ---- config (env; defaults come from the active network's table) ----
|
||||
const CONFIG = {
|
||||
rpcUrl: process.env.BNS_RPC_URL || "http://127.0.0.1:48332", // BCHN chipnet rpcport
|
||||
rpcUrl: process.env.BNS_RPC_URL || `http://127.0.0.1:${NETWORK.bchnRpcPort}`, // BCHN rpcport
|
||||
rpcUser: process.env.BNS_RPC_USER || "",
|
||||
rpcPass: process.env.BNS_RPC_PASS || "",
|
||||
rpcCookie: process.env.BNS_RPC_COOKIE || "", // path to .cookie (alternative to user/pass)
|
||||
wsBind: process.env.BNS_WS_BIND || "127.0.0.1",
|
||||
wsPort: Number(process.env.BNS_WS_PORT || 50010),
|
||||
// The beacon scripthash the resolver scans (constant; keep in sync with resolver-web.js).
|
||||
beacon: process.env.BNS_BEACON_SCRIPTHASH ||
|
||||
"e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b",
|
||||
// The beacon scripthash the resolver scans — from the network table unless
|
||||
// overridden (a mainnet instance before its beacon is pinned must set it).
|
||||
beacon: process.env.BNS_BEACON_SCRIPTHASH || NETWORK.beaconScripthash || "",
|
||||
// TLD-certificate beacon scripthash for TREG/TUPD (BNS2 TLD registry).
|
||||
// Defaults to the chipnet TLD beacon defined in src/config.js:
|
||||
// bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh
|
||||
// Set to "" to disable TLD indexing (the /tld-list.json endpoint then 503s).
|
||||
// See Decentralized.DNS/DESIGN-tld-registry.md §7.
|
||||
tldBeacon: process.env.BNS_TLD_BEACON_SCRIPTHASH ||
|
||||
"c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8",
|
||||
tldBeacon: process.env.BNS_TLD_BEACON_SCRIPTHASH ?? (NETWORK.tldBeaconScripthash || ""),
|
||||
// HTTP port for the /tld-list.json snapshot endpoint. 0 disables.
|
||||
httpPort: Number(process.env.BNS_HTTP_PORT || 0),
|
||||
httpBind: process.env.BNS_HTTP_BIND || "127.0.0.1",
|
||||
// Skip blocks before BNS existed on chipnet — set this to shrink the first scan.
|
||||
startHeight: Number(process.env.BNS_START_HEIGHT || 0),
|
||||
// Skip blocks before BNS existed on this network — shrinks the first scan.
|
||||
startHeight: Number(process.env.BNS_START_HEIGHT || NETWORK.startHeight || 0),
|
||||
pollMs: Number(process.env.BNS_POLL_MS || 10000),
|
||||
scanMempool: process.env.BNS_MEMPOOL === "1",
|
||||
stateFile: process.env.BNS_STATE_FILE || "./bns-indexer-state.json",
|
||||
|
|
|
|||
|
|
@ -15,15 +15,17 @@
|
|||
import WebSocket from "ws";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { BeaconIndex, startElectrumWS } from "./bns-indexer.js";
|
||||
import { NETWORK } from "../lib/resolver-web.js";
|
||||
|
||||
const CONFIG = {
|
||||
// Public (full) servers of the active network; our own beacon-only
|
||||
// indexers are excluded because this process IS one of them.
|
||||
servers: (process.env.BNS_ELECTRUM_SERVERS ||
|
||||
"wss://chipnet.bch.ninja:50004,wss://chipnet.imaginary.cash:50004,wss://chipnet.c3-soft.com:64004")
|
||||
NETWORK.electrum.filter((x) => !NETWORK.beaconOnly.test(x.url)).map((x) => x.url).join(","))
|
||||
.split(",").map((s) => s.trim()).filter(Boolean),
|
||||
wsBind: process.env.BNS_WS_BIND || "127.0.0.1",
|
||||
wsPort: Number(process.env.BNS_WS_PORT || 50010),
|
||||
beacon: process.env.BNS_BEACON_SCRIPTHASH ||
|
||||
"e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b",
|
||||
beacon: process.env.BNS_BEACON_SCRIPTHASH || NETWORK.beaconScripthash || "",
|
||||
pollMs: Number(process.env.BNS_POLL_MS || 30000),
|
||||
};
|
||||
const log = (...a) => console.log(new Date().toISOString(), ...a);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
binToHex,
|
||||
hexToBin,
|
||||
} from "@bitauth/libauth";
|
||||
import { NETWORK } from "./resolver-web.js";
|
||||
|
||||
// Electrum indexes by "scripthash": sha256(lockingBytecode), byte-reversed, hex.
|
||||
export function lockingBytecodeToScripthash(lockingBytecode) {
|
||||
|
|
@ -30,7 +31,7 @@ export function addressToScripthash(address) {
|
|||
// the flag only tells wallets the address is safe to receive CashTokens on.
|
||||
export function toTokenAddress(address) {
|
||||
const bytecode = addressToLockingBytecode(address);
|
||||
const prefix = String(address).includes(":") ? address.split(":")[0] : "bchtest";
|
||||
const prefix = String(address).includes(":") ? address.split(":")[0] : NETWORK.prefix;
|
||||
const r = lockingBytecodeToCashAddress({ bytecode, prefix, tokenSupport: true });
|
||||
if (typeof r === "string") throw new Error(`cannot token-encode "${address}": ${r}`);
|
||||
return r.address;
|
||||
|
|
@ -38,7 +39,7 @@ export function toTokenAddress(address) {
|
|||
|
||||
export function stripTokenAddress(address) {
|
||||
const bytecode = addressToLockingBytecode(address);
|
||||
const prefix = String(address).includes(":") ? address.split(":")[0] : "bchtest";
|
||||
const prefix = String(address).includes(":") ? address.split(":")[0] : NETWORK.prefix;
|
||||
const r = lockingBytecodeToCashAddress({ bytecode, prefix, tokenSupport: false });
|
||||
if (typeof r === "string") throw new Error(`cannot encode "${address}": ${r}`);
|
||||
return r.address;
|
||||
|
|
@ -100,7 +101,7 @@ export async function getTransaction(electrum, txid, verbose = true) {
|
|||
* non-standard script — returns null rather than a guess, because refunding to a
|
||||
* wrong address is worse than not refunding automatically.
|
||||
*/
|
||||
export function payerAddressFromInput(scriptSigHex, prefix = "bchtest") {
|
||||
export function payerAddressFromInput(scriptSigHex, prefix = NETWORK.prefix) {
|
||||
if (!scriptSigHex) return null;
|
||||
const script = hexToBin(scriptSigHex);
|
||||
const pushes = [];
|
||||
|
|
@ -129,7 +130,7 @@ export function payerAddressFromInput(scriptSigHex, prefix = "bchtest") {
|
|||
}
|
||||
|
||||
/** The paying address of a transaction, read from its first spendable input. */
|
||||
export async function payerAddressOf(electrum, txid, prefix = "bchtest") {
|
||||
export async function payerAddressOf(electrum, txid, prefix = NETWORK.prefix) {
|
||||
const tx = await getTransaction(electrum, txid, true);
|
||||
for (const vin of tx?.vin ?? []) {
|
||||
const addr = payerAddressFromInput(vin?.scriptSig?.hex, prefix);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
// `opts` is { broadcast, userPrompt }.
|
||||
|
||||
import { hash160, lockingBytecodeToCashAddress, binToHex } from "@bitauth/libauth";
|
||||
import { NETWORK } from "../resolver-web.js";
|
||||
|
||||
/** Compressed pubkey -> P2PKH locking bytecode. */
|
||||
export function pubkeyToLockingBytecode(publicKey) {
|
||||
|
|
@ -23,7 +24,7 @@ export function pubkeyToLockingBytecode(publicKey) {
|
|||
}
|
||||
|
||||
/** Compressed pubkey -> cashaddr for the given prefix. */
|
||||
export function pubkeyToAddress(publicKey, prefix = "bchtest", tokenSupport = false) {
|
||||
export function pubkeyToAddress(publicKey, prefix = NETWORK.prefix, tokenSupport = false) {
|
||||
const r = lockingBytecodeToCashAddress({
|
||||
bytecode: pubkeyToLockingBytecode(publicKey),
|
||||
prefix,
|
||||
|
|
|
|||
|
|
@ -6,21 +6,14 @@
|
|||
// into the extension / mobile webview), so the duplication is intentional —
|
||||
// change both together.
|
||||
|
||||
// Electrum servers speak wss on non-standard ports (50004, 62002). Corporate
|
||||
// networks, some VPNs and some mobile carriers block those outright, and when
|
||||
// they do, EVERY server in this list fails at once — one firewall rule takes the
|
||||
// whole resolver down. A server reachable on 443 is therefore worth more than
|
||||
// three more on 50004; put your own there and list it first.
|
||||
export const CHIPNET_ELECTRUM = [
|
||||
// Silent Mode's own bns-indexer proxied over standard HTTPS/443 via nginx
|
||||
// (bns-electrum-wss vhost extended into silentmode-st). This is the entry
|
||||
// that makes the resolver work from every network, including corporate
|
||||
// firewalls and mobile carriers that block the non-standard ports below.
|
||||
"wss://silentmode.st/electrum",
|
||||
"wss://chipnet.bch.ninja:50004",
|
||||
"wss://chipnet.imaginary.cash:50004",
|
||||
"wss://tbch4.loping.net:62002",
|
||||
];
|
||||
import { NETWORK, NETWORKS } from "./resolver-web.js";
|
||||
|
||||
// Server lists come from the network table in resolver-web.js (one source of
|
||||
// truth). Plain URL strings here; the resolver keeps the {url, ip} form.
|
||||
// A server reachable on 443 is worth more than three more on 50004, which is
|
||||
// why the table lists our own indexer first.
|
||||
export const CHIPNET_ELECTRUM = NETWORKS.chipnet.electrum.map((s) => s.url);
|
||||
export const ELECTRUM = NETWORK.electrum.map((s) => s.url);
|
||||
|
||||
export class Electrum {
|
||||
constructor(url, WebSocketImpl) {
|
||||
|
|
@ -89,7 +82,7 @@ export class Electrum {
|
|||
* and some mobile carriers block outright, so "all servers unreachable" is a
|
||||
* routine condition for real users and deserves an actionable message.
|
||||
*/
|
||||
export async function connectAny(urls = CHIPNET_ELECTRUM, WebSocketImpl) {
|
||||
export async function connectAny(urls = ELECTRUM, WebSocketImpl) {
|
||||
const failures = [];
|
||||
for (const url of urls) {
|
||||
try { return await new Electrum(url, WebSocketImpl).connect(); }
|
||||
|
|
@ -213,7 +206,7 @@ export class ElectrumPool {
|
|||
* defence, at the cost of more sockets). 1 disables cross-checking.
|
||||
* @param {number} [o.need] minimum before we give up entirely.
|
||||
*/
|
||||
export async function connectPool(urls = CHIPNET_ELECTRUM, WebSocketImpl, { want = 3, need = 1 } = {}) {
|
||||
export async function connectPool(urls = ELECTRUM, WebSocketImpl, { want = 3, need = 1 } = {}) {
|
||||
const settled = await Promise.all(
|
||||
urls.slice(0, Math.max(want, need)).map(async (url) => {
|
||||
try { return { ok: true, client: await new Electrum(url, WebSocketImpl).connect() }; }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// One-time setup: generate the registrant wallet and the registry beacon wallet,
|
||||
// persist seeds to wallets.json, and verify chipnet electrum connectivity.
|
||||
import { TestNetWallet } from "mainnet-js";
|
||||
import { Wallet, TestNetWallet } from "mainnet-js";
|
||||
import { NETWORK } from "./resolver-web.js";
|
||||
const WalletClass = NETWORK.id === "mainnet" ? Wallet : TestNetWallet;
|
||||
import { writeFileSync, existsSync, readFileSync } from "fs";
|
||||
|
||||
const FILE = new URL("../../wallets.json", import.meta.url);
|
||||
|
|
@ -9,8 +11,8 @@ let store;
|
|||
if (existsSync(FILE)) {
|
||||
store = JSON.parse(readFileSync(FILE, "utf8"));
|
||||
} else {
|
||||
const main = await TestNetWallet.newRandom();
|
||||
const beacon = await TestNetWallet.newRandom();
|
||||
const main = await WalletClass.newRandom();
|
||||
const beacon = await WalletClass.newRandom();
|
||||
store = {
|
||||
main: { seed: main.mnemonic, path: main.derivationPath },
|
||||
beacon: { seed: beacon.mnemonic, path: beacon.derivationPath },
|
||||
|
|
@ -18,8 +20,8 @@ if (existsSync(FILE)) {
|
|||
writeFileSync(FILE, JSON.stringify(store, null, 2));
|
||||
}
|
||||
|
||||
const main = await TestNetWallet.fromSeed(store.main.seed, store.main.path);
|
||||
const beacon = await TestNetWallet.fromSeed(store.beacon.seed, store.beacon.path);
|
||||
const main = await WalletClass.fromSeed(store.main.seed, store.main.path);
|
||||
const beacon = await WalletClass.fromSeed(store.beacon.seed, store.beacon.path);
|
||||
|
||||
console.log("network:", main.network);
|
||||
console.log("main cashaddr: ", main.cashaddr);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
OpcodesBCH, numberToBinUint32LE, binToNumberUint32LE, secp256k1, sha256,
|
||||
generateSigningSerializationBCH, encodeTransaction, flattenBinArray,
|
||||
} from "@bitauth/libauth";
|
||||
import { NETWORK } from "./resolver-web.js";
|
||||
import { ARTIFACT } from "./usd-listing-artifact.js";
|
||||
import { normalizeName, encodePayload, opReturnScript, selectInputs, BEACON, BEACON_DUST_SAT, CERTIFICATE_DUST_SAT, P2PKH_DUST_SAT } from "./register-tx.js";
|
||||
import { toInput, toSource, tokenOf, signInputs, SIGHASH_ALL_FORKID } from "./market-tx.js";
|
||||
|
|
@ -49,7 +50,7 @@ export function usdListingRedeem({ oraclePk, sellerPkh, targetCents, floorSats,
|
|||
]);
|
||||
}
|
||||
export const usdListingLock = (redeem) => encodeLockingBytecodeP2sh32(hash256(redeem));
|
||||
export function usdListingAddress(redeem, prefix = "bchtest") {
|
||||
export function usdListingAddress(redeem, prefix = NETWORK.prefix) {
|
||||
const r = lockingBytecodeToCashAddress({ bytecode: usdListingLock(redeem), prefix });
|
||||
if (typeof r === "string") throw new Error(r);
|
||||
return r.address;
|
||||
|
|
@ -234,7 +235,7 @@ export function verifyUsdListing(listing, { oraclePk } = {}) {
|
|||
if (oraclePk && listing.oracle_pk !== oraclePk) return fail("listing was made for a different oracle key");
|
||||
const redeem = usdListingRedeem({ oraclePk: listing.oracle_pk, sellerPkh: listing.seller_pkh, targetCents: listing.target_cents, floorSats: listing.floor_sats, ceilSats: listing.ceil_sats, maxAgeSecs: listing.max_age_secs });
|
||||
if (binToHex(redeem) !== listing.redeem_hex) return fail("redeem script does not match the stated terms");
|
||||
const prefix = String(listing.seller).split(":")[0] || "bchtest";
|
||||
const prefix = String(listing.seller).split(":")[0] || NETWORK.prefix;
|
||||
if (usdListingAddress(redeem, prefix) !== listing.covenant_address) return fail("covenant address does not match");
|
||||
if (binToHex(hash160(hexToBin(listing.seller_pkh))).length !== 40) return fail("bad seller pkh");
|
||||
if (listing.token?.category !== listing.category || !listing.token?.nft) return fail("token does not match category");
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
decodeHdPublicKey,
|
||||
secp256k1,
|
||||
} from "@bitauth/libauth";
|
||||
import { NETWORK, NETWORKS } from "./resolver-web.js";
|
||||
import { pubkeyToAddress } from "./connect/wallet-session.js";
|
||||
import { addressToLockingBytecode, toTokenAddress, getUtxos, hexToBin } from "./chain.js";
|
||||
import { normalizeName, displayName, encodeTransaction } from "./register-tx.js";
|
||||
|
|
@ -76,7 +77,7 @@ export const DEPOSIT_PATH = (index) => `m/44'/145'/0'/${DEPOSIT_BRANCH}/${index}
|
|||
* key present. This is the mode a public server should run in.
|
||||
* - `hdNode` — a private node, for the operator-side minting process.
|
||||
*/
|
||||
export function depositAddressAt(index, { xpub, hdNode, prefix = "bchtest" }) {
|
||||
export function depositAddressAt(index, { xpub, hdNode, prefix = NETWORK.prefix }) {
|
||||
if (xpub) {
|
||||
const decoded = decodeHdPublicKey(xpub);
|
||||
if (typeof decoded === "string") throw new Error(`bad xpub: ${decoded}`);
|
||||
|
|
@ -142,7 +143,7 @@ export function quotePrice({ serviceFeeSats, estimatedChainFeeSats = 500n, certi
|
|||
* normalise to the token-aware form ourselves rather than asking the buyer to
|
||||
* understand the difference between `q…` and `z…`.
|
||||
*/
|
||||
export function normaliseBuyerAddress(input, { prefix = "bchtest" } = {}) {
|
||||
export function normaliseBuyerAddress(input, { prefix = NETWORK.prefix } = {}) {
|
||||
const raw = String(input ?? "").trim();
|
||||
if (!raw) throw new Error("enter the address the name should be sent to");
|
||||
const withPrefix = raw.includes(":") ? raw : `${prefix}:${raw}`;
|
||||
|
|
@ -153,7 +154,7 @@ export function normaliseBuyerAddress(input, { prefix = "bchtest" } = {}) {
|
|||
// mistyped character when the real problem is that they are on the wrong chain.
|
||||
const gotPrefix = withPrefix.slice(0, withPrefix.indexOf(":")).toLowerCase();
|
||||
if (gotPrefix !== prefix) {
|
||||
const networkOf = (p) => (p === "bchtest" ? "chipnet" : p === "bitcoincash" ? "mainnet" : p);
|
||||
const networkOf = (p) => Object.values(NETWORKS).find((n) => n.prefix === p)?.label || p;
|
||||
throw new Error(
|
||||
`that is a ${networkOf(gotPrefix)} address, but this registry is on ${networkOf(prefix)} — ` +
|
||||
`a certificate sent there would be lost`,
|
||||
|
|
@ -220,7 +221,7 @@ export async function checkPayment(client, order) {
|
|||
}
|
||||
|
||||
/** The signing key for an order's deposit address — operator side only. */
|
||||
export function depositKeyAt(index, hdNode, prefix = "bchtest") {
|
||||
export function depositKeyAt(index, hdNode, prefix = NETWORK.prefix) {
|
||||
const child = deriveHdPath(hdNode, DEPOSIT_PATH(index));
|
||||
if (typeof child === "string") throw new Error(`derivation failed: ${child}`);
|
||||
const publicKey = secp256k1.derivePublicKeyCompressed(child.privateKey);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { NETWORK } from "./resolver-web.js";
|
||||
// The operator's name holdings, published and verifiable against the chain.
|
||||
//
|
||||
// WHY THIS EXISTS
|
||||
|
|
@ -118,7 +119,7 @@ function canonical(name) {
|
|||
* say" into "we said something untrue", and it is the omission a reader will find
|
||||
* first because the chain is public.
|
||||
*/
|
||||
export function renderPortfolio(report, { network = "chipnet" } = {}) {
|
||||
export function renderPortfolio(report, { network = NETWORK.id } = {}) {
|
||||
if (!report.complete) {
|
||||
const names = report.undisclosed.map((u) => u.name).join(", ");
|
||||
const err = new Error(
|
||||
|
|
|
|||
|
|
@ -37,16 +37,15 @@ import {
|
|||
encodeTransaction,
|
||||
decodeTransaction,
|
||||
} from "@bitauth/libauth";
|
||||
import { NETWORK } from "./resolver-web.js";
|
||||
|
||||
// ---------- protocol constants ----------
|
||||
|
||||
export const PREFIX = "BNS1";
|
||||
export const BEACON = "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl";
|
||||
// TLD-registry beacon (Decentralized.DNS/DESIGN-tld-registry.md §3). Keep in
|
||||
// step with Argus/src/config.js:TLD_BEACON and the indexer's default
|
||||
// BNS_TLD_BEACON_SCRIPTHASH — the three must agree, or clients disagree
|
||||
// about which TLD certificates exist.
|
||||
export const TLD_BEACON = "bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh";
|
||||
// Beacons of the active network — one table for every client, so builders,
|
||||
// resolvers and the indexer cannot disagree about which certificates exist.
|
||||
export const BEACON = NETWORK.beacon;
|
||||
export const TLD_BEACON = NETWORK.tldBeacon;
|
||||
export const BEACON_DUST_SAT = 600n;
|
||||
export const CERTIFICATE_DUST_SAT = 1000n; // token outputs need more than plain dust
|
||||
export const P2PKH_DUST_SAT = 546n;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
toWcTransactionObject,
|
||||
} from "./register-tx.js";
|
||||
import { signTransaction, txidOfHex } from "./sign-tx.js";
|
||||
import { buildIndex, fetchTldMap } from "./resolver-web.js";
|
||||
import { buildIndex, fetchTldMap, NETWORK } from "./resolver-web.js";
|
||||
import { REGISTRAR } from "../registrar-config.js";
|
||||
import { hexToBin, binToHex, decodeTransaction, lockingBytecodeToCashAddress, sha256, utf8ToBin, binToBase64 } from "@bitauth/libauth";
|
||||
import { signInputs, sourceOutputToJson, sourceOutputFromJson } from "./market-tx.js";
|
||||
|
|
@ -45,7 +45,7 @@ const TLD_OWNER_SHARE_DEN = 100n;
|
|||
* can't be decoded (in which case the caller should pay the full service
|
||||
* fee to the platform address as a fallback).
|
||||
*/
|
||||
export async function findTldOwnerAddress(client, tld, prefix = REGISTRAR.addressPrefix ?? "bchtest") {
|
||||
export async function findTldOwnerAddress(client, tld, prefix = REGISTRAR.addressPrefix) {
|
||||
const map = await fetchTldMap(client);
|
||||
const entry = map.get(String(tld).toLowerCase());
|
||||
if (!entry?.mintScriptHex) return null;
|
||||
|
|
@ -76,7 +76,7 @@ export function splitServiceFee(serviceFeeSats) {
|
|||
// fails with "-32601 unsupported method: blockchain.scripthash.listunspent".
|
||||
// Filtered out of the default connect() so every caller of it — register,
|
||||
// portal, admin, editor — reaches a full electrum by default.
|
||||
const BEACON_ONLY_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/;
|
||||
const BEACON_ONLY_URLS = NETWORK.beaconOnly;
|
||||
|
||||
/**
|
||||
* Open a connection to the chain. Caller must `.close()` it.
|
||||
|
|
|
|||
44
src/lib/resolver-web.d.ts
vendored
44
src/lib/resolver-web.d.ts
vendored
|
|
@ -32,10 +32,42 @@ export function parseTldsRecord(records: any): string[];
|
|||
export function getBcnrTlds(index: any): string[];
|
||||
export function fetchBcnrTlds(opts?: {}): Promise<string[]>;
|
||||
export function isBcnrNativeTld(tldOrList: any, tld: any): boolean;
|
||||
export const BEACON: "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl";
|
||||
export const BEACON_SCRIPTHASH: "e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b";
|
||||
export const TLD_BEACON: "bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh";
|
||||
export const TLD_BEACON_SCRIPTHASH: "c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8";
|
||||
export interface ElectrumServer { url: string; ip?: string }
|
||||
export interface BnsNetwork {
|
||||
id: "chipnet" | "mainnet";
|
||||
label: string;
|
||||
prefix: "bchtest" | "bitcoincash";
|
||||
beacon: string | null;
|
||||
beaconScripthash: string | null;
|
||||
tldBeacon: string | null;
|
||||
tldBeaconScripthash: string | null;
|
||||
electrum: ElectrumServer[];
|
||||
beaconOnly: RegExp;
|
||||
electrumListName: string;
|
||||
rootTldName: string;
|
||||
bootstrapTlds: string[];
|
||||
startHeight: number | null;
|
||||
hostingBucket: string;
|
||||
nameListUrl: string;
|
||||
cdnIndexUrl: string;
|
||||
nostrTags: { names: string; tlds: string };
|
||||
explorerTx: string;
|
||||
faucets: { name: string; url: string }[];
|
||||
operatorAddress: string | null;
|
||||
accountPath: string;
|
||||
bchnRpcPort: number;
|
||||
}
|
||||
export const NETWORKS: { chipnet: BnsNetwork; mainnet: BnsNetwork };
|
||||
export const DEFAULT_NETWORK_ID: "chipnet";
|
||||
export function networkIdFromEnv(): string;
|
||||
export function getNetwork(idOrRecord?: string | BnsNetwork): BnsNetwork;
|
||||
export const NETWORK: BnsNetwork;
|
||||
export function requireBeacons(network?: string | BnsNetwork): BnsNetwork;
|
||||
export const BEACON: string | null;
|
||||
export const BEACON_SCRIPTHASH: string | null;
|
||||
export const TLD_BEACON: string | null;
|
||||
export const TLD_BEACON_SCRIPTHASH: string | null;
|
||||
export const ELECTRUM: ElectrumServer[];
|
||||
export const PREFIX: "BNS1";
|
||||
export const TLD: ".bch";
|
||||
export const DEFAULT_TLD: "bch";
|
||||
|
|
@ -43,6 +75,6 @@ export const CHIPNET_ELECTRUM: {
|
|||
url: string;
|
||||
ip: string;
|
||||
}[];
|
||||
export const ELECTRUM_LIST_NAME: "electrum.silentmode.bch";
|
||||
export const ROOT_TLD_NAME: "tlds.bch";
|
||||
export const ELECTRUM_LIST_NAME: string;
|
||||
export const ROOT_TLD_NAME: string;
|
||||
export const BOOTSTRAP_TLDS: string[];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Portable BNS resolver — the shared keystone for all clients (browser
|
||||
// extension, mobile webview, Electron). ZERO dependencies: only a WebSocket
|
||||
// (native in browsers; inject Node's `ws`). Reads names straight from the
|
||||
// Bitcoin Cash chain via a chipnet electrum (Fulcrum) server, using verbose
|
||||
// Bitcoin Cash chain via an electrum (Fulcrum) server of the selected network, using verbose
|
||||
// transactions so CashToken data arrives as plain JSON — no tx-decoding lib.
|
||||
//
|
||||
// Contract (identical semantics to Argus/src/lib/bns.js):
|
||||
|
|
@ -9,17 +9,147 @@
|
|||
// resolveName("x.bch") -> Entry | null
|
||||
// normalizeName(input) -> string (throws on invalid)
|
||||
|
||||
export const BEACON = "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl";
|
||||
// electrum scripthash of BEACON (sha256(lockingBytecode) reversed, hex).
|
||||
// Constant — the beacon never changes.
|
||||
export const BEACON_SCRIPTHASH =
|
||||
"e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b";
|
||||
// TLD-certificate beacon (Decentralized.DNS/DESIGN-tld-registry.md §3).
|
||||
// Distinct from the name BEACON so clients can walk the TLD registry without
|
||||
// touching every name registration. Chipnet.
|
||||
export const TLD_BEACON = "bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh";
|
||||
export const TLD_BEACON_SCRIPTHASH =
|
||||
"c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8";
|
||||
// ---- networks: the ONE place chain-specific constants live -----------------
|
||||
// This file is copied verbatim into Ariadne and packaged into Theseus, so the
|
||||
// table lives here rather than in a sibling module. Every other Argus module
|
||||
// (registrar config, wallets, gateway, indexer, mirror scripts) imports from
|
||||
// here; the mobile resolver's BnsNetwork.java is generated from it
|
||||
// (Argus/src/generate-network-constants.mjs). Do not copy a value out of this
|
||||
// table into another file — reference it.
|
||||
//
|
||||
// Selecting a network: BNS_NETWORK (process env in Node, globalThis in a
|
||||
// browser) names the record; unset means chipnet. The mainnet record exists so
|
||||
// every code path is network-aware today, but its beacons, start height and
|
||||
// operator address are deliberately null until they are pinned, in the order
|
||||
// Decentralized.DNS/ROADMAP-MAINNET.md §6 requires. requireBeacons() is the
|
||||
// guard that turns "not pinned" into a clear error instead of a scan of nothing.
|
||||
export const NETWORKS = {
|
||||
chipnet: {
|
||||
id: "chipnet",
|
||||
label: "chipnet",
|
||||
prefix: "bchtest",
|
||||
// Name-registration beacon and its electrum scripthash
|
||||
// (sha256(lockingBytecode) reversed, hex). Constant — never changes.
|
||||
beacon: "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl",
|
||||
beaconScripthash: "e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b",
|
||||
// TLD-certificate beacon (Decentralized.DNS/DESIGN-tld-registry.md §3).
|
||||
// Distinct from the name beacon so clients can walk the TLD registry
|
||||
// without touching every name registration.
|
||||
tldBeacon: "bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh",
|
||||
tldBeaconScripthash: "c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8",
|
||||
// Each server carries a hostname `url` (TLS "just works") and an optional
|
||||
// pinned `ip`. DNS-independent clients (Theseus, via directIP) dial the ip
|
||||
// when the hostname cannot be resolved. Order matters: the pool prefers
|
||||
// earlier entries and takes the UNION of beacon history across everything
|
||||
// that answers. Our own indexer first, on 443, because every public server
|
||||
// listens on a port some firewalls block. The authoritative, updatable list
|
||||
// lives on-chain (electrumListName).
|
||||
electrum: [
|
||||
{ url: "wss://silentmode.st/electrum", ip: "195.184.247.106" },
|
||||
{ url: "wss://coinspectrum.duckdns.org:50011", ip: "195.184.247.106" },
|
||||
{ url: "wss://chipnet.bch.ninja:50004", ip: "135.148.236.244" },
|
||||
{ url: "wss://chipnet.imaginary.cash:50004", ip: "66.29.152.199" },
|
||||
{ url: "wss://tbch4.loping.net:62002", ip: "183.89.202.207" },
|
||||
],
|
||||
// Beacon-only indexers: they serve get_history for the beacons but not
|
||||
// listunspent, so wallet-side code skips them (matched against the url).
|
||||
beaconOnly: /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/,
|
||||
// Well-known names. A two-part `<label>.<tld>` name is distinct, a
|
||||
// three-part one collapses to its parent, so discovery names stay at label
|
||||
// depth under a real TLD.
|
||||
electrumListName: "electrum.bch",
|
||||
rootTldName: "tlds.bch",
|
||||
bootstrapTlds: ["bch"],
|
||||
// First block an indexer needs to scan.
|
||||
startHeight: 200000,
|
||||
// Sia bucket for operator-provided hosting (`<bucket>/<name>/`) and the
|
||||
// published snapshots; CDN copy and Nostr d-tags of the same lists.
|
||||
hostingBucket: "bns",
|
||||
nameListUrl: "https://s3.silentmode.st:8600/bns/name-list.json",
|
||||
cdnIndexUrl: "https://dl.silentmode.st/bns-name-index.json",
|
||||
nostrTags: { names: "bns-name-list", tlds: "bns-tld-list" },
|
||||
explorerTx: "https://chipnet.imaginary.cash/tx/",
|
||||
faucets: [
|
||||
{ name: "tbch.googol.cash", url: "https://tbch.googol.cash/" },
|
||||
{ name: "chipnet.bch.ninja faucet", url: "https://chipnet.bch.ninja/faucet" },
|
||||
],
|
||||
// Protocol steward: receives the platform share of service fees, may mint
|
||||
// TLDs below the floor, and owns the admin panel.
|
||||
operatorAddress: "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu",
|
||||
accountPath: "m/44'/145'/0'",
|
||||
bchnRpcPort: 48332,
|
||||
},
|
||||
mainnet: {
|
||||
id: "mainnet",
|
||||
label: "mainnet",
|
||||
prefix: "bitcoincash",
|
||||
// NOT PINNED. Pin all four together with startHeight and operatorAddress
|
||||
// in one commit, after the registry covenant address exists
|
||||
// (ROADMAP-MAINNET.md §6). Until then requireBeacons() refuses.
|
||||
beacon: null,
|
||||
beaconScripthash: null,
|
||||
tldBeacon: null,
|
||||
tldBeaconScripthash: null,
|
||||
// Public Fulcrum servers with CashTokens support (verified 2026-09-27).
|
||||
electrum: [
|
||||
{ url: "wss://bch.imaginary.cash:50004", ip: "67.223.119.97" },
|
||||
{ url: "wss://electrum.imaginary.cash:50004", ip: "15.204.113.162" },
|
||||
{ url: "wss://electroncash.dk:50004", ip: "87.63.200.138" },
|
||||
{ url: "wss://bch.loping.net:50004", ip: "183.89.202.207" },
|
||||
{ url: "wss://bch.soul-dev.com:50004", ip: "174.141.196.204" },
|
||||
{ url: "wss://cashnode.bch.ninja:50004", ip: "135.148.236.246" },
|
||||
],
|
||||
beaconOnly: /(?!)/, // matches nothing: no beacon-only indexer on mainnet yet
|
||||
electrumListName: "electrum.bch",
|
||||
rootTldName: "tlds.bch",
|
||||
bootstrapTlds: ["bch"],
|
||||
startHeight: null,
|
||||
// Separate bucket so a mainnet `space.x` can never collide with the
|
||||
// chipnet folder of the same name.
|
||||
hostingBucket: "bns-mainnet",
|
||||
nameListUrl: "https://s3.silentmode.st:8600/bns-mainnet/name-list.json",
|
||||
cdnIndexUrl: "https://dl.silentmode.st/bns-mainnet-name-index.json",
|
||||
nostrTags: { names: "bns-mainnet-name-list", tlds: "bns-mainnet-tld-list" },
|
||||
explorerTx: "https://blockchair.com/bitcoin-cash/transaction/",
|
||||
faucets: [],
|
||||
operatorAddress: null,
|
||||
accountPath: "m/44'/145'/0'",
|
||||
bchnRpcPort: 8332,
|
||||
},
|
||||
};
|
||||
export const DEFAULT_NETWORK_ID = "chipnet";
|
||||
// Which network this process/page runs on. Read once at import.
|
||||
export function networkIdFromEnv() {
|
||||
const fromGlobal = typeof globalThis !== "undefined" ? globalThis.BNS_NETWORK : null;
|
||||
const fromEnv = typeof process !== "undefined" && process.env ? process.env.BNS_NETWORK : null;
|
||||
return String(fromGlobal || fromEnv || DEFAULT_NETWORK_ID).trim().toLowerCase();
|
||||
}
|
||||
export function getNetwork(idOrRecord) {
|
||||
if (idOrRecord && typeof idOrRecord === "object") return idOrRecord;
|
||||
const id = idOrRecord || networkIdFromEnv();
|
||||
const n = NETWORKS[id];
|
||||
if (!n) throw new Error(`unknown network "${id}" (known: ${Object.keys(NETWORKS).join(", ")})`);
|
||||
return n;
|
||||
}
|
||||
export const NETWORK = getNetwork();
|
||||
// Throws when a network's beacons are not pinned yet — scanning a null
|
||||
// scripthash would silently return an empty registry.
|
||||
export function requireBeacons(network = NETWORK) {
|
||||
const n = getNetwork(network);
|
||||
if (!n.beaconScripthash || !n.tldBeaconScripthash) {
|
||||
throw new Error(`${n.label}: beacon addresses are not pinned yet (see ROADMAP-MAINNET.md §6)`);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
// Legacy names, kept so existing importers keep working. They describe the
|
||||
// ACTIVE network; CHIPNET_ELECTRUM is always the chipnet list.
|
||||
export const BEACON = NETWORK.beacon;
|
||||
export const BEACON_SCRIPTHASH = NETWORK.beaconScripthash;
|
||||
export const TLD_BEACON = NETWORK.tldBeacon;
|
||||
export const TLD_BEACON_SCRIPTHASH = NETWORK.tldBeaconScripthash;
|
||||
export const ELECTRUM = NETWORK.electrum;
|
||||
export const CHIPNET_ELECTRUM = NETWORKS.chipnet.electrum;
|
||||
export const ELECTRUM_LIST_NAME = NETWORK.electrumListName;
|
||||
export const PREFIX = "BNS1";
|
||||
export const TLD = ".bch";
|
||||
export const DEFAULT_TLD = "bch";
|
||||
|
|
@ -34,38 +164,6 @@ function canonicalKey(raw) {
|
|||
return raw.includes(".") ? raw : raw + "." + DEFAULT_TLD;
|
||||
}
|
||||
|
||||
// Each server carries a hostname `url` (used everywhere; TLS "just works") and an
|
||||
// optional pinned `ip`. DNS-independent clients (Theseus desktop, via directIP)
|
||||
// dial the ip directly when the hostname can't be resolved — so a dead system
|
||||
// resolver no longer stops Theseus from reaching the chain. The ip is only a
|
||||
// best-effort fallback: when DNS works the fresh hostname is always used, and the
|
||||
// authoritative, updatable server list lives on-chain (see ELECTRUM_LIST_NAME).
|
||||
export const CHIPNET_ELECTRUM = [
|
||||
// Silent Mode's own BCHN-backed BNS-only indexer, exposed on standard port
|
||||
// 443 so browsers on restrictive networks (corporate firewalls, mobile
|
||||
// carriers, VPNs) can reach it. Same backend as the :50011 endpoint below,
|
||||
// just proxied through the silentmode.st nginx on the normal https port.
|
||||
// Tried first so we prefer our own infrastructure on the most-reachable
|
||||
// route; the public servers below remain as fallbacks and cross-checks.
|
||||
{ url: "wss://silentmode.st/electrum", ip: "195.184.247.106" },
|
||||
{ url: "wss://coinspectrum.duckdns.org:50011", ip: "195.184.247.106" },
|
||||
{ url: "wss://chipnet.bch.ninja:50004", ip: "135.148.236.244" },
|
||||
{ url: "wss://chipnet.imaginary.cash:50004", ip: "66.29.152.199" },
|
||||
{ url: "wss://tbch4.loping.net:62002", ip: "183.89.202.207" },
|
||||
];
|
||||
// A well-known BCNR name whose record carries the current electrum server pool.
|
||||
// Resolved AFTER bootstrap (you need a server to read it) to refresh the list
|
||||
// without shipping a new build — the decentralized, ICANN-free source of truth.
|
||||
//
|
||||
// Choice of name is load-bearing. Under BNS's canonical-key rule, a two-part
|
||||
// name of the form `<label>.<tld>` is a distinct name, but a three-part name
|
||||
// like `electrum.silentmode.bch` collapses to its parent (`silentmode.bch`) —
|
||||
// it would inherit and OVERWRITE silentmode.bch's records on any UPD. So we
|
||||
// keep the discovery name at label depth under a real TLD: `electrum.bch`
|
||||
// normalises to bare `electrum` in the index, which does not conflict with
|
||||
// any existing name.
|
||||
export const ELECTRUM_LIST_NAME = "electrum.bch";
|
||||
|
||||
const hexToBytes = (h) => {
|
||||
const a = new Uint8Array(h.length / 2);
|
||||
for (let i = 0; i < a.length; i++) a[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
|
|
@ -274,10 +372,11 @@ function tldAdmits(tld, regHeight, tldMap) {
|
|||
// (see registrar.js findTldOwnerAddress). MVP: this is the ORIGINAL owner;
|
||||
// later transfers of the NFT aren't traced yet.
|
||||
// KEEP IN LOCKSTEP with bns.js buildTldMap — divergence is a consensus split.
|
||||
export async function fetchTldMap(el) {
|
||||
export async function fetchTldMap(el, network = NETWORK) {
|
||||
const { tldBeaconScripthash } = requireBeacons(network);
|
||||
const history = typeof el.unionHistory === "function"
|
||||
? (await el.unionHistory(TLD_BEACON_SCRIPTHASH)).history
|
||||
: await el.call("blockchain.scripthash.get_history", [TLD_BEACON_SCRIPTHASH]);
|
||||
? (await el.unionHistory(tldBeaconScripthash)).history
|
||||
: await el.call("blockchain.scripthash.get_history", [tldBeaconScripthash]);
|
||||
history.sort(byHeightThenTxid);
|
||||
const txs = new Map();
|
||||
for (const { tx_hash } of history) {
|
||||
|
|
@ -375,7 +474,8 @@ function ownerFromVout(vout) {
|
|||
return null;
|
||||
}
|
||||
|
||||
export async function buildIndex({ electrum = CHIPNET_ELECTRUM, WebSocket: WS, client, directIP = false, enforceTld = false, cosignGate = true } = {}) {
|
||||
export async function buildIndex({ network = NETWORK, electrum = getNetwork(network).electrum, WebSocket: WS, client, directIP = false, enforceTld = false, cosignGate = true } = {}) {
|
||||
const net = requireBeacons(network);
|
||||
const el = client ?? (await connectAny(electrum, WS, { directIP }));
|
||||
try {
|
||||
// Fetch the TLD map first (single electrum connection, one extra roundtrip).
|
||||
|
|
@ -383,7 +483,7 @@ export async function buildIndex({ electrum = CHIPNET_ELECTRUM, WebSocket: WS, c
|
|||
// gate when enforcing, and always for the co-sign rule (cosignGate).
|
||||
let tldMap = null;
|
||||
if (enforceTld || cosignGate) {
|
||||
try { tldMap = await fetchTldMap(el); }
|
||||
try { tldMap = await fetchTldMap(el, net); }
|
||||
catch (e) { if (enforceTld) throw e; tldMap = null; /* cosign gate degrades open if the TLD beacon is unreadable */ }
|
||||
}
|
||||
|
||||
|
|
@ -396,7 +496,7 @@ export async function buildIndex({ electrum = CHIPNET_ELECTRUM, WebSocket: WS, c
|
|||
let integrity = null;
|
||||
let history;
|
||||
if (typeof el.unionHistory === "function") {
|
||||
const merged = await el.unionHistory(BEACON_SCRIPTHASH);
|
||||
const merged = await el.unionHistory(net.beaconScripthash);
|
||||
history = merged.history;
|
||||
integrity = {
|
||||
servers: merged.servers,
|
||||
|
|
@ -404,7 +504,7 @@ export async function buildIndex({ electrum = CHIPNET_ELECTRUM, WebSocket: WS, c
|
|||
disagreements: merged.disagreements,
|
||||
};
|
||||
} else {
|
||||
history = await el.call("blockchain.scripthash.get_history", [BEACON_SCRIPTHASH]);
|
||||
history = await el.call("blockchain.scripthash.get_history", [net.beaconScripthash]);
|
||||
}
|
||||
history.sort(byHeightThenTxid);
|
||||
|
||||
|
|
@ -542,7 +642,7 @@ export async function resolveName(input, opts = {}) {
|
|||
// callers can do ad-hoc queries or drive a delta-refresh loop (fetch just the
|
||||
// tx bodies they don't already have). Same connection strategy buildIndex uses
|
||||
// — try hostname first, fall back to pinned-IP+SNI when directIP is on.
|
||||
export async function connectElectrum({ electrum = CHIPNET_ELECTRUM, WebSocket: WS, directIP = false } = {}) {
|
||||
export async function connectElectrum({ network = NETWORK, electrum = getNetwork(network).electrum, WebSocket: WS, directIP = false } = {}) {
|
||||
return connectAny(electrum, WS, { directIP });
|
||||
}
|
||||
|
||||
|
|
@ -577,11 +677,11 @@ export async function fetchElectrumServers(opts = {}) {
|
|||
// on-chain UPDATE (no software update, no reinstall). See
|
||||
// SilentMode/Argus/DESIGN-collision-modes.md for how clients use it (collision
|
||||
// detection, per-TLD defaults, preempting the soft "Open with…" prompt).
|
||||
export const ROOT_TLD_NAME = "tlds.bch";
|
||||
export const ROOT_TLD_NAME = NETWORK.rootTldName;
|
||||
// Shipped bootstrap set — used before the on-chain list is fetched, and as a
|
||||
// last-resort fallback if the root cert is briefly unreachable. Deliberately
|
||||
// small: just the TLD you're guaranteed to have.
|
||||
export const BOOTSTRAP_TLDS = ["bch"];
|
||||
export const BOOTSTRAP_TLDS = NETWORK.bootstrapTlds.slice();
|
||||
export function parseTldsRecord(records) {
|
||||
const raw = records && records.tlds;
|
||||
if (!raw || typeof raw !== "string") return [];
|
||||
|
|
@ -611,10 +711,10 @@ export async function fetchBcnrTlds(opts = {}) {
|
|||
// the daemon's isPermittedHost check. Falls back to BOOTSTRAP_TLDS if the
|
||||
// chain is unreachable (same shape as fetchBcnrTlds) — an installer that
|
||||
// cannot reach any electrum still gets a working (if minimal) root CA.
|
||||
export async function fetchTldList({ electrum = CHIPNET_ELECTRUM, WebSocket: WS, client, directIP = false } = {}) {
|
||||
export async function fetchTldList({ network = NETWORK, electrum = getNetwork(network).electrum, WebSocket: WS, client, directIP = false } = {}) {
|
||||
const el = client ?? (await connectAny(electrum, WS, { directIP }));
|
||||
try {
|
||||
const tldMap = await fetchTldMap(el);
|
||||
const tldMap = await fetchTldMap(el, network);
|
||||
const list = [...tldMap.keys()].sort();
|
||||
return list.length ? list : BOOTSTRAP_TLDS.slice();
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
lockingBytecodeToCashAddress,
|
||||
cashAddressToLockingBytecode,
|
||||
} from "@bitauth/libauth";
|
||||
import { NETWORK } from "./resolver-web.js";
|
||||
|
||||
const MAGIC = "Bitcoin Signed Message:\n";
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ export function messageMagicHash(message) {
|
|||
*
|
||||
* @returns {{address: string, publicKey: Uint8Array, compressed: boolean} | null}
|
||||
*/
|
||||
export function recoverSigner(message, signatureBase64, prefix = "bchtest") {
|
||||
export function recoverSigner(message, signatureBase64, prefix = NETWORK.prefix) {
|
||||
let sig;
|
||||
try {
|
||||
sig = base64ToBin(String(signatureBase64).trim());
|
||||
|
|
@ -103,7 +104,7 @@ export function recoverSigner(message, signatureBase64, prefix = "bchtest") {
|
|||
* address and its `q…` form are correctly treated as the same key — a buyer
|
||||
* signing with a wallet that only shows the legacy form must still pass.
|
||||
*/
|
||||
export function verifyControl({ message, signature, expectedAddress, prefix = "bchtest" }) {
|
||||
export function verifyControl({ message, signature, expectedAddress, prefix = NETWORK.prefix }) {
|
||||
const signer = recoverSigner(message, signature, prefix);
|
||||
if (!signer) return { ok: false, reason: "signature is malformed or not a Bitcoin message signature" };
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ import {
|
|||
binToBase64,
|
||||
base64ToBin,
|
||||
} from "@bitauth/libauth";
|
||||
import { NETWORK } from "./resolver-web.js";
|
||||
|
||||
export const CHIPNET_PREFIX = "bchtest";
|
||||
export const MAINNET_PREFIX = "bitcoincash";
|
||||
// Prefix of the active network (BNS_NETWORK; chipnet unless set).
|
||||
export const DEFAULT_PREFIX = NETWORK.prefix;
|
||||
// BCH coin type 145; account 0. Branch 0 = receive, 1 = change.
|
||||
export const ACCOUNT_PATH = "m/44'/145'/0'";
|
||||
// How many receiving addresses to watch. The flow funds and changes back to
|
||||
|
|
@ -60,7 +63,7 @@ export class BuiltInWallet {
|
|||
* on the BTC coin type, so BuiltInWallet can drive keys minted from the
|
||||
* CLI/mainnet-js path too.
|
||||
*/
|
||||
constructor(mnemonic, seed, prefix = CHIPNET_PREFIX, accountPath = ACCOUNT_PATH) {
|
||||
constructor(mnemonic, seed, prefix = DEFAULT_PREFIX, accountPath = ACCOUNT_PATH) {
|
||||
this.mnemonic = mnemonic;
|
||||
this.prefix = prefix;
|
||||
this.accountPath = accountPath;
|
||||
|
|
@ -69,12 +72,12 @@ export class BuiltInWallet {
|
|||
this._derive();
|
||||
}
|
||||
|
||||
static async create(prefix = CHIPNET_PREFIX, accountPath = ACCOUNT_PATH) {
|
||||
static async create(prefix = DEFAULT_PREFIX, accountPath = ACCOUNT_PATH) {
|
||||
const mnemonic = generateBip39Mnemonic();
|
||||
return new BuiltInWallet(mnemonic, await deriveSeedFromBip39Mnemonic(mnemonic), prefix, accountPath);
|
||||
}
|
||||
|
||||
static async fromMnemonic(mnemonic, prefix = CHIPNET_PREFIX, accountPath = ACCOUNT_PATH) {
|
||||
static async fromMnemonic(mnemonic, prefix = DEFAULT_PREFIX, accountPath = ACCOUNT_PATH) {
|
||||
const clean = String(mnemonic).trim().toLowerCase().replace(/\s+/g, " ");
|
||||
const seed = await deriveSeedFromBip39Mnemonic(clean);
|
||||
if (typeof seed === "string") throw new Error(`invalid recovery phrase: ${seed}`);
|
||||
|
|
@ -190,7 +193,7 @@ export class BuiltInWallet {
|
|||
} catch {
|
||||
throw new Error("wrong password");
|
||||
}
|
||||
return BuiltInWallet.fromMnemonic(new TextDecoder().decode(plain), o.prefix ?? CHIPNET_PREFIX);
|
||||
return BuiltInWallet.fromMnemonic(new TextDecoder().decode(plain), o.prefix ?? DEFAULT_PREFIX);
|
||||
}
|
||||
|
||||
async save(password, storage = globalThis.localStorage) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import "./dns-patch.js"; // must load before mainnet-js touches the network
|
||||
import { TestNetWallet } from "mainnet-js";
|
||||
import { Wallet, TestNetWallet } from "mainnet-js";
|
||||
import { NETWORK } from "./resolver-web.js";
|
||||
const WalletClass = NETWORK.id === "mainnet" ? Wallet : TestNetWallet;
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
||||
import { createInterface } from "node:readline";
|
||||
|
|
@ -84,7 +86,7 @@ export async function loadWallet(which = "main") {
|
|||
const store = JSON.parse(readFileSync(WALLET_FILE, "utf8"));
|
||||
if (store[which]?.seed) {
|
||||
const { seed, path } = store[which];
|
||||
return TestNetWallet.fromSeed(seed, path);
|
||||
return WalletClass.fromSeed(seed, path);
|
||||
}
|
||||
}
|
||||
// Fall-through: Deviant's cross-repo encrypted keystore.
|
||||
|
|
@ -93,8 +95,8 @@ export async function loadWallet(which = "main") {
|
|||
for (const slug of slugCandidates(which)) {
|
||||
const entry = dv.wallets[slug];
|
||||
if (!entry) continue;
|
||||
if (entry.seed) return TestNetWallet.fromSeed(entry.seed, entry.path ?? "m/44'/145'/0'/0/0");
|
||||
if (entry.wif) return TestNetWallet.fromWIF(entry.wif);
|
||||
if (entry.seed) return WalletClass.fromSeed(entry.seed, entry.path ?? "m/44'/145'/0'/0/0");
|
||||
if (entry.wif) return WalletClass.fromWIF(entry.wif);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -35,16 +35,16 @@ import { fileURLToPath } from "node:url";
|
|||
|
||||
import { loadWallet } from "./lib/wallet.js";
|
||||
import { buildNameSnapshot } from "./lib/snapshot-name.js";
|
||||
import { buildIndexFromSnapshot } from "./lib/resolver-web.js";
|
||||
import { buildIndexFromSnapshot, NETWORK, requireBeacons } from "./lib/resolver-web.js";
|
||||
import { BEACON, WALLET_FILE } from "./config.js";
|
||||
import { nostrKeyFromMnemonic } from "./lib/hermes-derive.js";
|
||||
|
||||
// The beacon's scripthash — the electrum layer indexes by scripthash, not
|
||||
// cashaddr. Kept in step with resolver-web.js:BEACON_SCRIPTHASH.
|
||||
const BEACON_SCRIPTHASH = "e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b";
|
||||
// cashaddr. From the network table (refuses while a network's beacon is unpinned).
|
||||
const BEACON_SCRIPTHASH = requireBeacons(NETWORK).beaconScripthash;
|
||||
|
||||
const DRY = process.env.BNS_MIRROR_DRY_RUN === "1";
|
||||
const BUCKET = process.env.BNS_MIRROR_SIA_BUCKET || "bns";
|
||||
const BUCKET = process.env.BNS_MIRROR_SIA_BUCKET || NETWORK.hostingBucket;
|
||||
const PREFIX = process.env.BNS_MIRROR_SIA_PREFIX || "";
|
||||
const RELAYS = (process.env.BNS_MIRROR_NOSTR_RELAYS || "wss://nos.lol,wss://relay.damus.io")
|
||||
.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
|
|
@ -151,7 +151,7 @@ try {
|
|||
// just leaves the previous CDN copy in place, and mobile keeps working
|
||||
// (bundled starter is still a valid fallback).
|
||||
const CDN_HOST = process.env.BNS_MIRROR_CDN_HOST || "silentmode";
|
||||
const CDN_PATH = process.env.BNS_MIRROR_CDN_PATH || "/opt/silent-mode/dl/bns-name-index.json";
|
||||
const CDN_PATH = process.env.BNS_MIRROR_CDN_PATH || "/opt/silent-mode/dl/" + NETWORK.cdnIndexUrl.split("/").pop();
|
||||
try {
|
||||
const tmp = path.join(mkdtempSync(path.join(tmpdir(), "bns-mirror-")), "bns-name-index.json");
|
||||
writeFileSync(tmp, mobileBody);
|
||||
|
|
@ -178,7 +178,7 @@ const content = JSON.stringify({
|
|||
});
|
||||
const createdAt = Math.floor(Date.parse(new Date().toISOString()) / 1000);
|
||||
const kind = 30078;
|
||||
const tags = [["d", "bns-name-list"]];
|
||||
const tags = [["d", NETWORK.nostrTags.names]];
|
||||
const sk = Uint8Array.from(Buffer.from(skHex, "hex"));
|
||||
const event = finalizeEvent({ kind, created_at: createdAt, tags, content }, sk);
|
||||
log(`nostr event id : ${event.id}`);
|
||||
|
|
|
|||
|
|
@ -30,11 +30,12 @@ import { finalizeEvent } from "nostr-tools/pure";
|
|||
import { loadWallet } from "./lib/wallet.js";
|
||||
import { buildTldSnapshot } from "./lib/tld-index.js";
|
||||
import { TLD_BEACON, WALLET_FILE } from "./config.js";
|
||||
import { NETWORK } from "./lib/resolver-web.js";
|
||||
import { nostrKeyFromMnemonic } from "./lib/hermes-derive.js";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const DRY = process.env.BNS_MIRROR_DRY_RUN === "1";
|
||||
const BUCKET = process.env.BNS_MIRROR_SIA_BUCKET || "bns";
|
||||
const BUCKET = process.env.BNS_MIRROR_SIA_BUCKET || NETWORK.hostingBucket;
|
||||
const PREFIX = process.env.BNS_MIRROR_SIA_PREFIX || "";
|
||||
const RELAYS = (process.env.BNS_MIRROR_NOSTR_RELAYS || "wss://nos.lol,wss://relay.damus.io")
|
||||
.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
|
|
@ -128,7 +129,7 @@ const content = JSON.stringify({
|
|||
});
|
||||
const createdAt = Math.floor(Date.parse(new Date().toISOString()) / 1000);
|
||||
const kind = 30078;
|
||||
const tags = [["d", "bns-tld-list"]];
|
||||
const tags = [["d", NETWORK.nostrTags.tlds]];
|
||||
|
||||
// nostr-tools' finalizeEvent handles both NIP-01 id derivation and BIP-340
|
||||
// schnorr signing. BCH's own schnorr (libauth's signMessageHashSchnorr) uses
|
||||
|
|
|
|||
|
|
@ -2,15 +2,18 @@
|
|||
// network live. Everything here is public information; no secrets (SECURITY.md
|
||||
// rule 0). This file is bundled into the public register page as-is.
|
||||
|
||||
import { CHIPNET_ELECTRUM } from "./lib/electrum.js";
|
||||
import { ELECTRUM } from "./lib/electrum.js";
|
||||
import { NETWORK } from "./lib/resolver-web.js";
|
||||
|
||||
export const REGISTRAR = {
|
||||
// ---- network -----------------------------------------------------------
|
||||
// CHIPNET ONLY. Mainnet is a separate, deliberate switch: it needs mainnet
|
||||
// electrum servers, the "bitcoincash:" prefix, a mainnet beacon address and
|
||||
// real money. Do not flip this without re-reading REGISTRATION-WALLET-PROMPT.md.
|
||||
network: "chipnet",
|
||||
addressPrefix: "bchtest",
|
||||
// Everything chain-specific is read from the network table in
|
||||
// lib/resolver-web.js (selected by BNS_NETWORK; chipnet unless set). Mainnet
|
||||
// stays a deliberate switch: its beacons are unpinned until
|
||||
// ROADMAP-MAINNET.md §6 is done, and requireBeacons() refuses before that.
|
||||
network: NETWORK.id,
|
||||
addressPrefix: NETWORK.prefix,
|
||||
explorerTx: NETWORK.explorerTx,
|
||||
|
||||
// ---- chain connectivity -------------------------------------------------
|
||||
// Order matters: the pool connects in parallel but prefers earlier entries,
|
||||
|
|
@ -29,11 +32,12 @@ export const REGISTRAR = {
|
|||
// See OPERATIONS-electrum.md for running one.
|
||||
selfHosted: null,
|
||||
get electrum() {
|
||||
return this.selfHosted ? [this.selfHosted, ...CHIPNET_ELECTRUM] : CHIPNET_ELECTRUM;
|
||||
return this.selfHosted ? [this.selfHosted, ...ELECTRUM] : ELECTRUM;
|
||||
},
|
||||
|
||||
// ---- discovery ---------------------------------------------------------
|
||||
beacon: "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl",
|
||||
beacon: NETWORK.beacon,
|
||||
tldBeacon: NETWORK.tldBeacon,
|
||||
|
||||
// ---- price -------------------------------------------------------------
|
||||
// The service fee is a plain output inside the buyer's own minting
|
||||
|
|
@ -41,7 +45,7 @@ export const REGISTRAR = {
|
|||
// certificate is minted straight to the buyer's key in the same transaction
|
||||
// that pays us. If `sats` is 0n the output is omitted entirely.
|
||||
serviceFee: {
|
||||
address: "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu",
|
||||
address: NETWORK.operatorAddress,
|
||||
sats: 10_000n, // chipnet placeholder price — set real pricing at mainnet
|
||||
},
|
||||
|
||||
|
|
@ -64,11 +68,8 @@ export const REGISTRAR = {
|
|||
dappIcon: "https://silentmode.st/icon.png",
|
||||
},
|
||||
|
||||
// Faucets shown to a buyer whose new wallet is empty (chipnet only).
|
||||
faucets: [
|
||||
{ name: "tbch.googol.cash", url: "https://tbch.googol.cash/" },
|
||||
{ name: "chipnet.bch.ninja faucet", url: "https://chipnet.bch.ninja/faucet" },
|
||||
],
|
||||
// Faucets shown to a buyer whose new wallet is empty (empty on mainnet).
|
||||
faucets: NETWORK.faucets,
|
||||
};
|
||||
|
||||
/** Human-readable price breakdown for the confirmation screen. */
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ import path from "node:path";
|
|||
import { loadWallet } from "./lib/wallet.js";
|
||||
import { buildNameSnapshot } from "./lib/snapshot-name.js";
|
||||
import { BEACON } from "./config.js";
|
||||
import { NETWORK, requireBeacons } from "./lib/resolver-web.js";
|
||||
|
||||
const BEACON_SCRIPTHASH = "e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b";
|
||||
const BEACON_SCRIPTHASH = requireBeacons(NETWORK).beaconScripthash;
|
||||
const OUT = process.argv[2] || fileURLToPath(
|
||||
new URL("../../TheseusNavigator/snapshots/bns-name-snapshot.json", import.meta.url),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// Bundled by build-web.mjs to site/js/bns-register.js as a plain ES module:
|
||||
// <script type="module"> import * as BNS from './js/bns-register.js' </script>
|
||||
|
||||
export { BuiltInWallet, CHIPNET_PREFIX } from "../lib/wallet-web.js";
|
||||
export { BuiltInWallet, CHIPNET_PREFIX, MAINNET_PREFIX, DEFAULT_PREFIX } from "../lib/wallet-web.js";
|
||||
export {
|
||||
connect,
|
||||
checkAvailability,
|
||||
|
|
@ -43,7 +43,7 @@ export { buildListing, completeSale, verifyListing, signInputs, sourceOutputToJs
|
|||
export { findTldCertificateUtxo, buildCosignRequest, signCosignRequest } from "../lib/registrar.js";
|
||||
export { usdListingRedeem, usdListingAddress, payoutSats, parseOracleMessage, verifyOracleMessage, buildUsdListingTx, buildUsdBuyTx, buildUsdCancelTx, verifyUsdListing, DEFAULT_MAX_AGE_SECS } from "../lib/market-usd.js";
|
||||
export { toTokenAddress, addressToLockingBytecode, addressToScripthash, getUtxos, getUtxosForAddresses, broadcast } from "../lib/chain.js";
|
||||
export { resolveName, buildIndex } from "../lib/resolver-web.js";
|
||||
export { resolveName, buildIndex, NETWORK, NETWORKS, getNetwork, requireBeacons, DEFAULT_NETWORK_ID } from "../lib/resolver-web.js";
|
||||
export {
|
||||
describeGenesisRequirement,
|
||||
pubkeyToAddress,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue