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:
Local Dev 2026-09-27 21:18:37 +02:00
parent 890af362b8
commit 341803133b
12 changed files with 47 additions and 42 deletions

View file

@ -432,7 +432,7 @@ $("mint-btn").addEventListener("click", async () => {
<script src="../js/pricing.js?v=20260920bits"></script>
<script defer src="../js/site-footer.js?v=20260907rel"></script>
<script defer src="../js/theme.js?v=20260921mob"></script>
<script defer src="../js/profile-menu.js?v=20260921mob"></script>
<script defer src="../js/profile-menu.js?v=20260927net"></script>
<script>
// Sidenav scroll-spy: highlight the section currently in view.
// Uses scroll-position tracking so short sections at the end of the page

View file

@ -651,7 +651,7 @@ process.exit(0);"</pre>
<script src="../js/pricing.js?v=20260920bits"></script>
<script defer src="../js/site-footer.js?v=20260917market"></script>
<script defer src="../js/theme.js?v=20260921mob"></script>
<script defer src="../js/profile-menu.js?v=20260921mob"></script>
<script defer src="../js/profile-menu.js?v=20260927net"></script>
<script>
// Sidenav scroll-spy: highlight the section currently in view.
// Uses scroll-position tracking so short sections at the end of the page

View file

@ -556,14 +556,14 @@ process.exit(0);"</pre>
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script>
<script type="module" src="./js/register-flow.js?v=20260920usd"></script>
<script type="module" src="./js/register-flow.js?v=20260927net"></script>
<script src="./js/pricing.js?v=20260920bits"></script>
<script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script>
<script defer src="./js/theme.js?v=20260921mob"></script>
<script defer src="./js/site-footer.js?v=20260917market"></script>
<script defer src="./js/profile-menu.js?v=20260921mob"></script>
<script defer src="./js/profile-menu.js?v=20260927net"></script>
<script>
// Sidenav scroll-spy: highlight the section currently in view.
// Uses scroll-position tracking so short sections at the end of the page

View file

@ -10,12 +10,16 @@
// is the persistent device session (js/session.js): sign in once, come back
// without a prompt until you sign out.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd";
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260927net";
const API = "https://silentmode.st";
const TLD_API = "https://navigate.st/api/tlds?include_hidden=1";
// Operator wallet (chipnet): shows the Admin panel link in the sidebar.
const OPERATOR_ADDRESS = "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu";
// Chain-specific values (prefix, explorer, hosting bucket, operator) come from
// the bundle's network table; plain scripts on the page read window.siriusNetwork.
const NET = BNS.NETWORK;
window.siriusNetwork = NET;
// Operator wallet: shows the Admin panel link in the sidebar.
const OPERATOR_ADDRESS = NET.operatorAddress;
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@ -55,7 +59,7 @@ async function restoreSession() {
try {
const s = await S.restore();
if (!s?.mnemonic) return null;
const w = await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.CHIPNET_PREFIX, s.accountPath || undefined);
const w = await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, NET.prefix, s.accountPath || undefined);
window.siriusWallet = w;
return w;
} catch { return null; }
@ -283,7 +287,7 @@ function pointsTo(rec) {
}
// The public relay is navigate.st; silentmode.st is the operator host.
const siteUrl = (name) => `https://navigate.st/bns/${encodeURIComponent(name)}/`;
const txUrl = (txid) => `https://chipnet.imaginary.cash/tx/${txid}`;
const txUrl = (txid) => `${NET.explorerTx}${txid}`;
// ---------- attention strip (top of Domain names) ----------
function renderAttention() {
@ -311,7 +315,7 @@ function renderStudio() {
status.textContent = `Pick a name to build or edit its site.`;
list.innerHTML = [...H.names].sort((a, b) => a.name.localeCompare(b.name)).map((e) => {
const rec = e.records || {};
const studioFolder = `bns/${e.name}/`;
const studioFolder = `${NET.hostingBucket}/${e.name}/`;
const st = rec.s3 === studioFolder ? { label: "built with Studio", cls: "b-ok" }
: rec.s3 ? { label: "site on Sia (external)", cls: "b-x" }
: rec.h ? { label: "inline HTML", cls: "b-x" }
@ -566,7 +570,7 @@ const CT_KEYS = ["h", "s3", "p", "ip", "tls"];
// s3 is offered as a choice: "Sirius.X hosting" is the name's own folder on
// Sia (bns/<name>/), free, filled by Studio or the upload API. The raw key
// stays reachable under "advanced" for anyone hosting from another folder.
const hostingFolder = () => `bns/${current.name}/`;
const hostingFolder = () => `${NET.hostingBucket}/${current.name}/`;
const S3_HINTS = {
off: "Nothing is served from Sia. Use inline HTML, a proxy, your own server or a redirect — or switch on Sirius.X hosting.",
sirius: "Free web hosting by Sirius.X: the files are served from your own folder on Sia storage. Build the page in Studio (Publish to Sia) or upload files yourself; only your key can write there. DNS records live in the same folder.",
@ -613,7 +617,7 @@ async function publishRecords(records, logId, msgId, btnId, doneText) {
btn.disabled = true; btn.textContent = "Signing…"; setMsg(msgId, "");
let el = null;
try {
el = await BNS.connect(); push("connected to chipnet");
el = await BNS.connect(); push(`connected to ${NET.label}`);
const res = wallet.source === "wc"
? await BNS.setRecordsWithExternalWallet(el, { session: wallet.session, name: current.name, records, onProgress: (s) => push(s) })
: await BNS.setRecordsWithBuiltInWallet(el, { wallet, name: current.name, records, onProgress: (s) => push(s) });
@ -645,13 +649,13 @@ $("tr-send").addEventListener("click", async () => {
if (!current.entry || !wallet) return;
const to = $("tr-addr").value.trim();
if (wallet.source === "wc") return setMsg("tr-msg", "Transfers from an external wallet land in the next drop — sign in with the built-in wallet.", "err");
if (!/^bchtest:/.test(to)) return setMsg("tr-msg", "Chipnet addresses start with bchtest: — a mainnet address would lose the name.", "err");
if (!to.startsWith(NET.prefix + ":")) return setMsg("tr-msg", `${NET.label} addresses start with ${NET.prefix}: — an address from another network would lose the name.`, "err");
if (!confirm(`Transfer ${current.name} to ${to}? This cannot be undone.`)) return;
const push = logger("tr-log"); const btn = $("tr-send");
btn.disabled = true; btn.textContent = "Transferring…"; setMsg("tr-msg", "");
let el = null;
try {
el = await BNS.connect(); push("connected to chipnet");
el = await BNS.connect(); push(`connected to ${NET.label}`);
const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
const raw = BNS.normalizeName(current.name);
const commitment = [...new TextEncoder().encode(raw)].map((b) => b.toString(16).padStart(2, "0")).join("");
@ -751,7 +755,7 @@ $("sell-list").addEventListener("click", async () => {
const push = logger("sell-log"); const btn = $("sell-list"); btn.disabled = true; btn.textContent = "Signing…"; setMsg("sell-msg", "");
let el = null;
try {
el = await BNS.connect(); push("connected to chipnet");
el = await BNS.connect(); push(`connected to ${NET.label}`);
const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
const raw = BNS.normalizeName(current.name);
const commitment = [...new TextEncoder().encode(raw)].map((b) => b.toString(16).padStart(2, "0")).join("");
@ -802,7 +806,7 @@ $("sell-cancel").addEventListener("click", async () => {
if (activeListing.kind === "usd") {
let el = null;
try {
el = await BNS.connect(); push("connected to chipnet");
el = await BNS.connect(); push(`connected to ${NET.label}`);
const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
const built = BNS.buildUsdCancelTx({ listing: activeListing, sellerAddress: wallet.address, sellerTokenAddress: wallet.tokenAddress, utxos, records: current.entry.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode, keyFor: (h) => wallet.keyFor(h) });
const txid = await BNS.broadcast(el, built.hex);
@ -1041,7 +1045,7 @@ $("tld-ed-save").addEventListener("click", async () => {
$("tld-ed-save").disabled = true; $("tld-ed-save").textContent = "Signing…"; setMsg("tld-ed-msg", "");
let el = null;
try {
el = await BNS.connect(); push("connected to chipnet");
el = await BNS.connect(); push(`connected to ${NET.label}`);
const res = await BNS.setTldRecordsWithBuiltInWallet(el, { wallet, tld: tldEditor.tld, records, category: tldEditor.category, onProgress: (s) => push(String(s)) });
push("broadcast: " + (res.txid || "(no txid)"));
const t = H.tlds.find((x) => x.tld === tldEditor.tld); if (t) t.records = records;
@ -1083,7 +1087,7 @@ $("tld-submit").addEventListener("click", async () => {
$("tld-submit").disabled = true; $("tld-submit").textContent = "Signing…";
let el = null;
try {
el = await BNS.connect(); push("connected to chipnet");
el = await BNS.connect(); push(`connected to ${NET.label}`);
const serviceFee = { address: BNS.REGISTRAR?.serviceFee?.address ?? null, sats };
const args = { tld: label, records: {}, serviceFee, tldListUrl: "https://navigate.st/api/tlds", onProgress: (s) => push(s) };
const res = wallet.source === "wc"
@ -1096,7 +1100,7 @@ $("tld-submit").addEventListener("click", async () => {
} catch (e) {
msg.style.color = "var(--taken)"; const raw = String(e.message || e);
msg.innerHTML = /wallet is empty/i.test(raw)
? `Failed: wallet is empty. Fund <code style="font-size:11.5px">${esc(wallet.address)}</code> with chipnet coins first — free from <a href="https://tbch.googol.cash/" target="_blank" rel="noopener">tbch.googol.cash</a>.`
? `Failed: wallet is empty. Fund <code style="font-size:11.5px">${esc(wallet.address)}</code> with ${esc(NET.label)} coins first — free from <a href="https://tbch.googol.cash/" target="_blank" rel="noopener">tbch.googol.cash</a>.`
: "Failed: " + esc(raw);
push("error: " + raw); $("tld-submit").disabled = false; updateTldPrice();
} finally { try { el?.close?.(); } catch {} }

View file

@ -7,7 +7,7 @@
// stays valid while their certificate UTXO is unspent, so the gateway prunes
// stale offers and the buyer's broadcast is the final arbiter.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd";
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260927net";
const API = "https://silentmode.st";
const $ = (id) => document.getElementById(id);
@ -21,6 +21,7 @@ const satsForCents = (cents) => Math.round(Number(cents) / 100 * (window.siriusP
const clampSats = (sats, l) => Math.min(Number(l.ceil_sats), Math.max(Number(l.floor_sats), sats));
const shortAddr = (a) => (a ? a.replace(/^[^:]+:/, "").slice(0, 8) + "…" + a.slice(-5) : "—");
window.siriusNetwork = BNS.NETWORK;
let listings = [];
let wallet = null;
let selected = null;
@ -32,7 +33,7 @@ async function restoreWallet() {
try {
const s = await S.restore();
if (!s?.mnemonic) return null;
const w = await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.CHIPNET_PREFIX, s.accountPath || undefined);
const w = await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.NETWORK.prefix, s.accountPath || undefined);
window.siriusWallet = w;
return w;
} catch { return null; }
@ -118,7 +119,7 @@ $("buy-go").addEventListener("click", async () => {
const btn = $("buy-go"); btn.disabled = true; btn.textContent = "Buying…"; setMsg("");
let el = null;
try {
el = await BNS.connect(); push("connected to chipnet");
el = await BNS.connect(); push(`connected to ${BNS.NETWORK.label}`);
const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
push(`wallet has ${utxos.filter((u) => !u.token).length} spendable coin(s)`);
let built, signed;

View file

@ -70,7 +70,7 @@
const held = info ? `${info.names ?? 0} name${info.names === 1 ? "" : "s"} · ${info.tlds ?? 0} TLD${info.tlds === 1 ? "" : "s"}` : "open the dashboard to load";
menu.innerHTML = `
<div class="profile-header">
<span class="profile-hint">Signed in via ${escapeAttr(p.source === "wc" ? "WizardConnect" : "seed phrase")} · chipnet</span>
<span class="profile-hint">Signed in via ${escapeAttr(p.source === "wc" ? "WizardConnect" : "seed phrase")} · ${escapeAttr(window.siriusNetwork?.label || "chipnet")}</span>
<code>${escapeAttr(p.address)}</code>
</div>
<div class="wallet-stats">
@ -89,10 +89,10 @@
<span class="mi-ic">🎟</span>
<span class="mi-body"><b>Copy token address</b><span class="mi-hint">where name certificates are minted</span></span>
</a>` : ""}
<a href="https://tbch.googol.cash/" target="_blank" rel="noopener" role="menuitem">
${(window.siriusNetwork ? window.siriusNetwork.faucets.length > 0 : true) ? `<a href="${escapeAttr(window.siriusNetwork?.faucets?.[0]?.url || "https://tbch.googol.cash/")}" target="_blank" rel="noopener" role="menuitem">
<span class="mi-ic">🚰</span>
<span class="mi-body"><b>Get test coins</b><span class="mi-hint">chipnet faucet</span></span>
</a>
<span class="mi-body"><b>Get test coins</b><span class="mi-hint">${escapeAttr(window.siriusNetwork?.label || "chipnet")} faucet</span></span>
</a>` : ""}
<a href="${PORTAL_URL}#settings" role="menuitem">
<span class="mi-ic">⚙</span>
<span class="mi-body"><b>Wallet settings</b><span class="mi-hint">session, PIN, forget device</span></span>

View file

@ -7,7 +7,7 @@
// Injects its own modal HTML into <body> and its own CSS into <head> on load
// so the host page needs nothing but a single script include.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd";
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260927net";
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c]));
@ -530,7 +530,7 @@ function stepWallet() {
<span>Cashonize 0.9+ or Paytaca. Encrypted end-to-end over Nostr; the relay sees only
ciphertext. Your keys never leave your wallet.</span></span></button>
<div class="note" style="margin-top:18px">Everything below runs on <b>chipnet</b>, Bitcoin Cash's
<div class="note" style="margin-top:18px">Everything below runs on <b>${esc(BNS.NETWORK.label)}</b>, Bitcoin Cash's
test network. The coins are free and worth nothing — this is an alpha, and it is the honest
place to try it before real money is involved.</div>
`);
@ -611,7 +611,7 @@ async function stepExternalConfirm(session, label) {
if (balance.sats === 0n) {
render(`<h3>That wallet is empty</h3>
<p class="sub">Fund <span class="mono">${esc(owner)}</span> with chipnet coins and reconnect.</p>
<p class="sub">Fund <span class="mono">${esc(owner)}</span> with ${esc(BNS.NETWORK.label)} coins and reconnect.</p>
<div class="row end"><button class="btn ghost" id="back">Back</button></div>`);
$("back").onclick = async () => { try { await session.disconnect(); } catch {} stepWallet(); };
return;
@ -886,7 +886,7 @@ async function stepFund() {
<p class="sub">Send test coins to this address. It is yours — derived from your phrase.</p>
<div class="addr"><code id="a">${esc(w.address)}</code>
<button class="btn ghost" id="copy">Copy</button></div>
<div class="muted" style="margin-top:10px">Free chipnet coins: ${faucets}</div>
${faucets ? `<div class="muted" style="margin-top:10px">Free ${esc(BNS.NETWORK.label)} coins: ${faucets}</div>` : ""}
<div class="note" id="bal" style="margin-top:16px">checking balance…</div>
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="next" disabled>Continue →</button></div>
@ -968,7 +968,7 @@ async function stepConfirmTld() {
<p class="sub">One transaction. It mints the TLD certificate to <b>your</b> token address; you become
the registry operator for every second-level name under it.</p>
<table class="price">
<tr><td>Service fee${feeSats === 0n ? " <small>(none on chipnet)</small>" : ""}</td>
<tr><td>Service fee${feeSats === 0n ? " <small>(none on ${esc(BNS.NETWORK.label)})</small>" : ""}</td>
<td>${bits(feeSats)}</td></tr>
<tr><td>Beacon dust + chain fee <small>(covered by wallet)</small></td>
<td>~13 bits</td></tr>
@ -992,7 +992,7 @@ async function stepConfirmTld() {
// opts: { feeSats, kind: "name"|"tld", back, next }
function stepApprove(opts) {
const { feeSats, kind, back, next } = opts;
const feeLine = feeSats && feeSats > 0n ? `<b class="ok">${bits(feeSats)}</b>` : `<b class="ok">Service fee waived on chipnet</b>`;
const feeLine = feeSats && feeSats > 0n ? `<b class="ok">${bits(feeSats)}</b>` : `<b class="ok">Service fee waived on ${esc(BNS.NETWORK.label)}</b>`;
const target = kind === "tld" ? `.<b>${esc(state.name)}</b>` : `<b>${esc(state.name)}</b>`;
// Signed in with a live wallet and no "ask for PIN before payments"

View file

@ -11,14 +11,14 @@
// The gateway never holds a key: it verifies each upload's signature
// against the current NFT owner and refuses writes outside bns/<name>/.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd";
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260927net";
import { initAssistant } from "./studio-ai.js?v=20260920ai";
const API = "https://silentmode.st";
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const name = (new URLSearchParams(location.search).get("name") || "").toLowerCase().trim();
const folder = name ? `bns/${name}/` : null;
const folder = name ? `${BNS.NETWORK.hostingBucket}/${name}/` : null;
const readUrl = (path) => `${API}/api/site/${encodeURIComponent(name)}/${path}`;
const siteUrl = () => `${API}/bns/${encodeURIComponent(name)}/`;
@ -41,7 +41,7 @@ async function restoreWallet() {
try {
const s = await S.restore();
if (!s?.mnemonic) return null;
return await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.CHIPNET_PREFIX, s.accountPath || undefined);
return await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.NETWORK.prefix, s.accountPath || undefined);
} catch { return null; }
}

View file

@ -151,9 +151,9 @@
</script>
<script src="../js/session.js?v=20260917dash"></script>
<script src="../js/pricing.js?v=20260920bits"></script>
<script type="module" src="../js/market.js?v=20260920usd"></script>
<script type="module" src="../js/market.js?v=20260927net"></script>
<script defer src="../js/theme.js?v=20260921mob"></script>
<script defer src="../js/site-footer.js?v=20260917market"></script>
<script defer src="../js/profile-menu.js?v=20260921mob"></script>
<script defer src="../js/profile-menu.js?v=20260927net"></script>
</body>
</html>

View file

@ -704,10 +704,10 @@
<script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script>
<script src="./js/pricing.js?v=20260920bits"></script>
<script type="module" src="./js/dashboard.js?v=20260921usd4"></script>
<script type="module" src="./js/dashboard.js?v=20260927net"></script>
<script defer src="./js/theme.js?v=20260921mob"></script>
<script defer src="./js/site-footer.js?v=20260917market"></script>
<script type="module" src="./js/register-flow.js?v=20260920usd"></script>
<script defer src="./js/profile-menu.js?v=20260921mob"></script>
<script defer src="./js/profile-menu.js?v=20260927net"></script>
</body>
</html>

View file

@ -312,6 +312,6 @@
<script src="./vendor/grapesjs/style-bg.js?v=2.0.2"></script>
<script src="./vendor/grapesjs/touch.js?v=0.1.1"></script>
<script src="./vendor/grapesjs/parser-postcss.js?v=1.0.3"></script>
<script type="module" src="./js/studio.js?v=20260921pages"></script>
<script type="module" src="./js/studio.js?v=20260927net"></script>
</body>
</html>

View file

@ -348,13 +348,13 @@
})();
</script>
<script type="module" src="./js/register-flow.js?v=20260920usd"></script>
<script type="module" src="./js/register-flow.js?v=20260927net"></script>
<script src="./js/pricing.js?v=20260920bits"></script>
<script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script>
<script defer src="./js/theme.js?v=20260921mob"></script>
<script defer src="./js/site-footer.js?v=20260917market"></script>
<script defer src="./js/profile-menu.js?v=20260921mob"></script>
<script defer src="./js/profile-menu.js?v=20260927net"></script>
<script>
// Sidenav scroll-spy: highlight the section currently in view.
// Uses scroll-position tracking so short sections at the end of the page