BNS: Sia+Nostr snapshot mirror, Theseus warm-start, VPS playbook, electrum.bch

Four independent pieces of one story — make cold-start .bch resolution fast,
survive individual electrum outages, and turn adding a Silent Mode BNS server
into a checklist run.

* Argus/src/lib/snapshot-name.js — pure, interpretation-free snapshot format
  for the name beacon (raw history + verbose tx cache + deterministic root).
* Argus/src/publish-name-mirror.mjs — mirror the snapshot to Sia S3
  (floating + content-addressed) and Nostr NIP-33 (kind 30078,
  d-tag bns-name-list). Same pattern as publish-tld-mirror.mjs.
* Argus/src/snapshot-name-to-file.mjs — dump the snapshot to a file for
  bundling. Writes to TheseusNavigator/snapshots/bns-name-snapshot.json.
* Argus/src/lib/resolver-web.js — new export buildIndexFromSnapshot() runs
  the same REG/UPD reduction as buildIndex() but from a pre-fetched snapshot,
  no electrum required. Warm-start path in Theseus depends on this.
* TheseusNavigator/main.js — warmFromSnapshot() and refreshSnapshotFromSia()
  wired into app.whenReady(). First .bch navigation returns from a warm index
  instead of waiting on a full electrum walk.
* TheseusNavigator/package.json — bundles the starter snapshot as an
  extraResource so packaged builds ship with a floor.
* TheseusNavigator/snapshots/bns-name-snapshot.json — first starter snapshot
  (70 beacon txs, root 8cc859aa…).
* VPS/BNS-SERVER-PLAYBOOK.md — turn-key checklist for spinning up a second
  or third Silent Mode BNS server, distilled from what actually worked on
  the silentmode box (BCHN 29 [chip]-not-[chipnet], multi-beacon indexer,
  wss:// nginx, on-chain publish via electrum.bch, common gotchas table).
* Argus/src/register-electrum-list.mjs — publish the current chipnet
  server pool as `el` on electrum.bch. Fits inside the 200-byte OP_RETURN
  cap by sizing the record (drops fallback IP pins in favour of URL-only
  tokens until they fit). NAME was chosen deliberately: two-part `electrum.bch`
  normalises to the bare distinct name `electrum`, whereas the earlier
  candidate `electrum.silentmode.bch` collapsed to silentmode.bch and would
  have inherited/overwritten that name's records.
* Argus/src/revert-silentmode-bch.mjs — one-off recovery from the earlier
  register-electrum-list revision that hit exactly that subdomain-collapse
  bug (broadcast an UPD to silentmode.bch and clobbered its s3 record).
  Records recovered from the local snapshot cache; safe to leave in the
  tree as documentation of what happened.
This commit is contained in:
Local Dev 2026-08-30 09:59:01 +02:00
parent 9068eff9c7
commit f29b82a50f
6 changed files with 473 additions and 1 deletions

View file

@ -52,7 +52,15 @@ export const CHIPNET_ELECTRUM = [
// 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.
export const ELECTRUM_LIST_NAME = "electrum.silentmode.bch";
//
// 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);
@ -376,6 +384,59 @@ export async function buildIndex({ electrum = CHIPNET_ELECTRUM, WebSocket: WS, c
}
}
// Build the index from a pre-fetched snapshot ({ history, txs }) — the shape
// publish-name-mirror.mjs writes to Sia and Theseus caches on disk. Pure
// function: no electrum, no fetch, no I/O. Runs the SAME reduction (sort,
// dep-order, first-REG-wins, UPD-with-category-proof) so any consumer that
// walks `snapshot.txs` through this converges on the same names Map as the
// live buildIndex would from the same underlying chain state.
//
// Signature deliberately mirrors buildIndex so it's a drop-in for callers
// that already had one (Theseus's ensureIndex warm-start path).
export function buildIndexFromSnapshot({ snapshot, enforceTld = false, tldMap = null } = {}) {
if (!snapshot || !Array.isArray(snapshot.history)) {
throw new Error("buildIndexFromSnapshot: snapshot with `history` array required");
}
if (enforceTld && !tldMap) {
// Snapshots don't embed the TLD map — the caller would have to load a TLD
// snapshot separately and pass it in. Off by default to keep the fast-path
// free of surprise dependencies.
throw new Error("buildIndexFromSnapshot: enforceTld=true requires an explicit tldMap");
}
const txMap = snapshot.txs instanceof Map ? snapshot.txs : new Map(Object.entries(snapshot.txs || {}));
const history = [...snapshot.history].sort(byHeightThenTxid);
const names = new Map();
for (const { tx_hash, height } of orderByDependency(history.filter((h) => txMap.has(h.tx_hash)), txMap)) {
const tx = txMap.get(tx_hash);
const vouts = tx.vout || [];
let payload = null;
for (const v of vouts) payload = parseOpReturnHex(v.scriptPubKey?.hex) ?? payload;
if (!payload) continue;
const rawName = payload.n;
if (!RAWNAME_RE.test(rawName)) continue;
const key = canonicalKey(rawName);
if (payload.o === "REG") {
if (names.has(key)) continue;
if (enforceTld && !tldAdmits(tldOfKey(key), height, tldMap)) continue;
const commitHex = strToHex(rawName);
const mint = vouts.find((v) => v.tokenData?.nft && v.tokenData.nft.commitment === commitHex);
if (!mint) continue;
names.set(key, {
name: key, category: mint.tokenData.category, records: payload.r ?? {},
txid: tx_hash, height, updatedTxid: null,
});
} else if (payload.o === "UPD") {
const entry = names.get(key);
if (!entry) continue;
const proves = vouts.some((v) => v.tokenData && v.tokenData.category === entry.category);
if (!proves) continue;
entry.records = payload.r ?? {};
entry.updatedTxid = tx_hash;
}
}
return names;
}
export async function resolveName(input, opts = {}) {
const name = normalizeName(input);
const index = await buildIndex(opts);

96
src/lib/snapshot-name.js Normal file
View file

@ -0,0 +1,96 @@
// Snapshot of the BNS name beacon: raw history + verbose tx cache, plus a
// deterministic root over the ordered history. Consumed by Theseus and the
// mobile resolver to skip the "walk the whole beacon on cold start" round
// trip — buildIndex (bns.js / resolver-web.js) reads the txs directly.
//
// Design goals:
// 1. **Interpretation-free.** The snapshot is the raw beacon history + tx
// cache. It embeds ZERO indexer-side interpretation (no REG/UPD/first-wins
// decisions), so consumers built on any resolver version still work
// against it. Adding a new opcode does not require re-mirroring.
// 2. **Deterministic root.** Any honest producer looking at the same chain
// state up to (asOfHeight, asOfTxid) produces byte-identical bytes,
// hence the same sha256 root. This is what a client pins.
// 3. **Delta-friendly.** history is sorted (height, txid). To catch up, a
// consumer asks any electrum for the FULL history and skips any (tx_hash)
// already in the snapshot. It only fetches the verbose tx for the new
// ones. Big win because getting a verbose tx is the expensive round-trip.
//
// Schema slug is baked in so future breaking changes can migrate cleanly.
import { createHash } from "node:crypto";
export const NAME_SNAPSHOT_SCHEMA = "bns-name-snapshot/1";
function byHeightThenTxid(a, b) {
const ha = a.height > 0 ? a.height : Infinity;
const hb = b.height > 0 ? b.height : Infinity;
return ha - hb || (a.tx_hash < b.tx_hash ? -1 : 1);
}
// Canonicalise the history: (height, txid)-sorted, minimal object shape.
// Root is over THIS, not txs — the txs are already content-addressed by their
// txids, which appear in history verbatim. A tampered tx cache still parses
// as valid JSON but breaks buildIndex visibly on the consumer.
function canonicaliseHistory(history) {
const sorted = [...history].sort(byHeightThenTxid).map((h) => ({
tx_hash: String(h.tx_hash),
height: Number(h.height),
}));
return JSON.stringify(sorted);
}
/**
* Build a name-beacon snapshot.
*
* @param {object} inputs
* @param {string} inputs.beacon - beacon scripthash, hex.
* @param {Array<{tx_hash: string, height: number}>} inputs.history - electrum-shape history.
* @param {Map<string, object> | Record<string, object>} inputs.txs - verbose tx cache keyed by txid.
* @param {number|null} [inputs.asOfHeight] - BCHN tip at snapshot time (may be null).
* @param {string|null} [inputs.asOfTxid] - hash of block at asOfHeight (may be null).
* @returns {{ schema: string, beacon: string, root: string, asOfHeight: number|null, asOfTxid: string|null, history: Array, txs: Object }}
*/
export function buildNameSnapshot({ beacon, history = [], txs, asOfHeight = null, asOfTxid = null } = {}) {
if (!beacon || typeof beacon !== "string") throw new Error("buildNameSnapshot: beacon (scripthash hex) required");
const txMap = txs instanceof Map ? Object.fromEntries(txs) : (txs || {});
const canon = canonicaliseHistory(history);
const root = createHash("sha256").update(canon, "utf8").digest("hex");
return {
schema: NAME_SNAPSHOT_SCHEMA,
beacon,
root,
asOfHeight,
asOfTxid,
history: JSON.parse(canon), // return the sorted normalised copy — same bytes the root is over
txs: txMap,
};
}
// Merge a snapshot with fresh electrum history to catch up without re-fetching
// every verbose tx. Returns `{ history, txs, addedTxids }` where txs is the
// UNION (snapshot cache + newly fetched), so buildIndex can walk it directly.
//
// `fetchTx(txid) -> Promise<verboseTx>` is provided by the caller (electrum
// client, HTTP fetch of raw hex + decode, whatever); this function stays I/O-
// agnostic. It is safe to pass the same snapshot's history as freshHistory —
// the result is a no-op merge.
export async function mergeFreshHistory({ snapshot, freshHistory, fetchTx }) {
const known = new Set(snapshot.history.map((h) => h.tx_hash));
const merged = new Map(snapshot.history.map((h) => [h.tx_hash, h]));
const txs = new Map(Object.entries(snapshot.txs || {}));
const addedTxids = [];
for (const h of freshHistory) {
if (!known.has(h.tx_hash)) {
addedTxids.push(h.tx_hash);
try { txs.set(h.tx_hash, await fetchTx(h.tx_hash)); }
catch { /* unreadable — skip; consumer's buildIndex ignores missing tx */ }
}
merged.set(h.tx_hash, { tx_hash: h.tx_hash, height: h.height });
}
return {
history: [...merged.values()].sort(byHeightThenTxid),
txs,
addedTxids,
};
}

156
src/publish-name-mirror.mjs Normal file
View file

@ -0,0 +1,156 @@
// Publish the current NAME-beacon snapshot to public mirrors, alongside the
// TLD-beacon mirror (publish-tld-mirror.mjs). Same shape:
// • Sia S3 — floating key (bns/name-list.json) and content-addressed
// immutable key (bns/name-list-<root>.json). Consumers pin the root; the
// floating key is the "latest" pointer they refresh on a cron.
// • Nostr — NIP-33 replaceable event (kind 30078, d-tag bns-name-list)
// carrying {root, asOfHeight, asOfTxid, count, sia_url}, signed by the
// operator's Nostr key (Hermes derivation from the wallet seed).
//
// Why mirror the name beacon at all: a Theseus/mobile install today walks the
// whole beacon on cold start via electrum. The verbose-tx fetches are the
// expensive step and repeat verbatim on every device. Publishing the raw
// history + tx cache means a client's cold start becomes:
// GET floating pointer (a few KB)
// → GET immutable snapshot for that root (~350 KB @ 72 beacon txs today)
// → electrum delta for whatever landed after asOfHeight
// → local buildIndex on the union
// Rounds trips: three, all cacheable. Same trust model as before — the raw
// beacon txs are still cryptographic evidence; the mirror is just a fast lane.
//
// Environment:
// BNS_MIRROR_SIA_BUCKET default "bns"
// BNS_MIRROR_SIA_PREFIX default "" — set to "name/" to nest
// BNS_MIRROR_NOSTR_RELAYS default "wss://nos.lol,wss://relay.damus.io"
// BNS_MIRROR_DRY_RUN=1 build + hash + log, do NOT publish anywhere
import { readFileSync } from "node:fs";
import { S3Client, PutObjectCommand, HeadBucketCommand, CreateBucketCommand } from "@aws-sdk/client-s3";
import WebSocket from "ws";
import { finalizeEvent } from "nostr-tools/pure";
import { fileURLToPath } from "node:url";
import { loadWallet } from "./lib/wallet.js";
import { buildNameSnapshot } from "./lib/snapshot-name.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";
const DRY = process.env.BNS_MIRROR_DRY_RUN === "1";
const BUCKET = process.env.BNS_MIRROR_SIA_BUCKET || "bns";
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);
const log = (...a) => console.log(new Date().toISOString(), ...a);
// ---------- 1) build snapshot ----------
log("building snapshot from name beacon", BEACON, "…");
const wallet = await loadWallet("main");
const history = await wallet.provider.getHistory(BEACON);
log(`beacon history : ${history.length} txs`);
const txs = new Map();
for (const { tx_hash } of history) {
try { txs.set(tx_hash, await wallet.provider.getRawTransactionObject(tx_hash)); }
catch { /* skip; the snapshot format tolerates missing tx entries */ }
}
log(`verbose txs : ${txs.size}/${history.length} fetched`);
const snap = buildNameSnapshot({
beacon: BEACON_SCRIPTHASH,
history,
txs,
// asOfHeight/asOfTxid: no BCHN tip here (wallet's electrum provider doesn't
// expose it), so nulls. The indexer's own publisher will fill these when we
// migrate the publisher to run VPS-side against the local BCHN.
asOfHeight: null,
asOfTxid: null,
});
const body = JSON.stringify(snap);
log(`snapshot bytes : ${Buffer.byteLength(body)}, root=${snap.root}`);
if (DRY) {
log("BNS_MIRROR_DRY_RUN=1 — not publishing.");
process.exit(0);
}
// ---------- 2) publish to Sia (floating + content-addressed) ----------
let siaUrl = null;
try {
const cfg = JSON.parse(readFileSync(new URL("../sia-s3.json", import.meta.url), "utf8"));
const s3 = new S3Client({
endpoint: cfg.endpoint ?? "http://127.0.0.1:8000",
region: "us-east-1",
forcePathStyle: true,
credentials: { accessKeyId: cfg.accessKey, secretAccessKey: cfg.secretKey },
});
try { await s3.send(new HeadBucketCommand({ Bucket: BUCKET })); }
catch { await s3.send(new CreateBucketCommand({ Bucket: BUCKET })); log(`created bucket "${BUCKET}"`); }
const floating = `${PREFIX}name-list.json`;
const immutable = `${PREFIX}name-list-${snap.root}.json`;
for (const key of [floating, immutable]) {
try {
await s3.send(new PutObjectCommand({
Bucket: BUCKET, Key: key, Body: body,
ContentType: "application/json",
CacheControl: key === floating ? "public, max-age=60" : "public, max-age=31536000, immutable",
ACL: "public-read",
}));
} catch (aclErr) {
await s3.send(new PutObjectCommand({
Bucket: BUCKET, Key: key, Body: body,
ContentType: "application/json",
CacheControl: key === floating ? "public, max-age=60" : "public, max-age=31536000, immutable",
}));
log(` (ACL=public-read rejected for ${key}: ${aclErr.message} — uploaded without ACL; operator must set bucket policy)`);
}
log(` uploaded s3://${BUCKET}/${key}`);
}
siaUrl = `${cfg.endpoint.replace(/\/$/, "")}/${BUCKET}/${immutable}`;
log(`sia URL (immutable): ${siaUrl}`);
} catch (e) {
log(`SIA UPLOAD FAILED: ${e.message}`);
log("continuing to Nostr anyway — event without sia_url still carries the root");
}
// ---------- 3) publish NIP-33 event to Nostr ----------
const store = JSON.parse(readFileSync(fileURLToPath(WALLET_FILE), "utf8"));
const { skHex, pkHex } = await nostrKeyFromMnemonic(store.main.seed);
log(`nostr pubkey : ${pkHex}`);
const content = JSON.stringify({
root: snap.root,
asOfHeight: snap.asOfHeight,
asOfTxid: snap.asOfTxid,
count: snap.history.length,
sia_url: siaUrl,
});
const createdAt = Math.floor(Date.parse(new Date().toISOString()) / 1000);
const kind = 30078;
const tags = [["d", "bns-name-list"]];
const sk = Uint8Array.from(Buffer.from(skHex, "hex"));
const event = finalizeEvent({ kind, created_at: createdAt, tags, content }, sk);
log(`nostr event id : ${event.id}`);
async function publishOne(url) {
return new Promise((resolve) => {
const ws = new WebSocket(url);
const done = (status, detail) => { try { ws.close(); } catch {} resolve({ url, status, detail }); };
ws.on("open", () => { ws.send(JSON.stringify(["EVENT", event])); });
ws.on("message", (d) => {
let msg; try { msg = JSON.parse(d.toString()); } catch { return; }
if (msg[0] === "OK" && msg[1] === event.id) done(msg[2] ? "ok" : "rejected", msg[3] || "");
});
ws.on("error", (e) => done("error", e.message));
setTimeout(() => done("timeout", ""), 8000);
});
}
const results = await Promise.all(RELAYS.map(publishOne));
for (const r of results) log(` relay ${r.url.padEnd(30)} → ${r.status}${r.detail ? " (" + r.detail + ")" : ""}`);
const okCount = results.filter((r) => r.status === "ok").length;
log(`nostr: ${okCount}/${results.length} relays accepted`);
process.exit(0);

View file

@ -0,0 +1,84 @@
// Publish the current chipnet electrum-server pool to the on-chain name
// `electrum.silentmode.bch`, so clients read the fresh list at bootstrap and
// refresh their pool without shipping a new build. Idempotent: if the name
// already exists, does an UPD; otherwise a REG.
//
// node src/register-electrum-list.mjs
//
// The pool source of truth here is the CHIPNET_ELECTRUM seed in
// resolver-web.js — keep them in step. Adding a new VPS: append to the seed
// AND re-run this. The `el` record format is a space-separated list of
// `wss://host:port|ip` tokens (ip optional; if given, clients can dial
// DNS-independent via SNI).
import { loadWallet } from "./lib/wallet.js";
import { registerName, updateName, buildIndex, normalizeName } from "./lib/bns.js";
import { CHIPNET_ELECTRUM } from "./lib/resolver-web.js";
// electrum.bch — normalises to bare `electrum` in the index, a distinct name
// with no collision. NOT `electrum.silentmode.bch`, which collapses to
// silentmode.bch under normalizeName's <sub>.<label>.<tld> rule and would
// overwrite that name's records on UPD. (History: earlier revision of this
// script did exactly that and had to revert. See revert-silentmode-bch.mjs.)
const NAME = "electrum.bch";
// Build the `el` string exactly as the resolver expects: "wss://host:port|ip"
// tokens, space-separated. Order matches the seed so the on-chain list and
// the shipped fallback agree on priority.
function toElRecord(servers) {
return servers
.filter((s) => /^wss?:\/\//i.test(s.url))
.map((s) => s.ip ? `${s.url}|${s.ip}` : s.url)
.join(" ");
}
// The OP_RETURN payload is capped at 200 bytes by encodePayload. Envelope
// (BNS1 + JSON overhead) for `electrum.silentmode.bch` eats ~60 bytes, so
// the `el` value has ~140 bytes to work with. That's enough for the primary
// server with its IP pin plus 1-2 public fallbacks unpinned. Drop the
// remaining fallbacks from the record — they still live in CHIPNET_ELECTRUM
// as the shipped seed, so no client loses reachability from this trim.
function fitToPayloadBudget(servers, budget = 140) {
const out = [];
let used = 0;
for (const s of servers) {
if (!/^wss?:\/\//i.test(s.url)) continue;
const tok = s.ip ? `${s.url}|${s.ip}` : s.url;
const cost = tok.length + (out.length ? 1 : 0); // +space between tokens
if (used + cost > budget) continue;
out.push(tok);
used += cost;
}
return out.join(" ");
}
const el = fitToPayloadBudget(CHIPNET_ELECTRUM);
if (!el) { console.error("CHIPNET_ELECTRUM is empty — nothing to publish"); process.exit(1); }
console.log("target name :", NAME);
console.log("record :", el);
console.log("record bytes :", Buffer.byteLength(el), "(cap ~140)");
const wallet = await loadWallet("main");
console.log("from wallet :", wallet.cashaddr);
const bal = await wallet.getBalance("sat");
console.log("wallet balance :", bal, "sat\n");
const key = normalizeName(NAME); // "electrum.silentmode.bch" (or bare label if <TLD>==bch)
const idx = await buildIndex(wallet.provider);
const existing = idx.get(key);
let result;
if (existing) {
console.log(`existing REG at height ${existing.height} — signing UPD…`);
result = await updateName(wallet, NAME, { el });
} else {
console.log("no existing REG — minting fresh…");
result = await registerName(wallet, NAME, { el });
}
console.log("\n✅ broadcast");
console.log(" name :", result.name);
console.log(" txid :", result.txid);
console.log(" category :", result.category);
console.log("\nTakes effect at the next chipnet block (~seconds). Clients that");
console.log("resolve", NAME, "after that will refresh their pool from this record.");
process.exit(0);

View file

@ -0,0 +1,27 @@
// One-off: revert silentmode.bch's records to the state before the accidental
// UPD in txid 4fa572b2337ec7844364bfe9fea613b2f8283aba7d7dd46ede73c807c2834987.
// That UPD happened because bns.js:normalizeName collapses "electrum.silentmode.bch"
// down to "silentmode" (subdomain of a .bch name → parent name), which the
// register-electrum-list script did not account for. The prior on-chain
// records were {"s3":"bns/silentmode/"} — recovered from the local name
// snapshot (Argus/src/snapshot-name-to-file.mjs) which was generated BEFORE
// the accidental UPD.
//
// After this runs, register-electrum-list.mjs needs a rework that stops
// treating "electrum.silentmode.bch" as a single BCNR name (it isn't — under
// the .bch TLD it's a subdomain that shares silentmode.bch's records). The
// real fix is to move ELECTRUM_LIST_NAME to a name whose canonical key is
// distinct — e.g. "electrum.sm" once .sm is TREG'd, or "electrum-list.bch".
import { loadWallet } from "./lib/wallet.js";
import { updateName } from "./lib/bns.js";
const wallet = await loadWallet("main");
console.log("from wallet :", wallet.cashaddr);
console.log("reverting silentmode.bch records → {\"s3\":\"bns/silentmode/\"} …");
const result = await updateName(wallet, "silentmode", { s3: "bns/silentmode/" });
console.log("\n✅ broadcast");
console.log(" name :", result.name);
console.log(" txid :", result.txid);
console.log(" category :", result.category);
process.exit(0);

View file

@ -0,0 +1,48 @@
// Build the name-beacon snapshot and write it to a local file. Same code
// path publish-name-mirror.mjs uses; skips Sia/Nostr publishing. Useful for
// bundling a starter snapshot into Theseus at build time.
//
// node src/snapshot-name-to-file.mjs [outfile]
//
// Default outfile: TheseusNavigator/snapshots/bns-name-snapshot.json
// (the path main.js reads as SNAPSHOT_BUNDLED).
import { writeFileSync, mkdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { loadWallet } from "./lib/wallet.js";
import { buildNameSnapshot } from "./lib/snapshot-name.js";
import { BEACON } from "./config.js";
const BEACON_SCRIPTHASH = "e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b";
const OUT = process.argv[2] || fileURLToPath(
new URL("../../TheseusNavigator/snapshots/bns-name-snapshot.json", import.meta.url),
);
const log = (...a) => console.log(new Date().toISOString(), ...a);
log("building snapshot from name beacon", BEACON, "…");
const wallet = await loadWallet("main");
const history = await wallet.provider.getHistory(BEACON);
log(`beacon history : ${history.length} txs`);
const txs = new Map();
for (const { tx_hash } of history) {
try { txs.set(tx_hash, await wallet.provider.getRawTransactionObject(tx_hash)); }
catch { /* skip */ }
}
log(`verbose txs : ${txs.size}/${history.length}`);
const snap = buildNameSnapshot({
beacon: BEACON_SCRIPTHASH,
history,
txs,
asOfHeight: null,
asOfTxid: null,
});
const body = JSON.stringify(snap);
mkdirSync(path.dirname(OUT), { recursive: true });
writeFileSync(OUT, body);
log(`wrote ${Buffer.byteLength(body)} bytes to ${OUT}`);
log(`root: ${snap.root}`);
process.exit(0);