// Sirius.X dashboard — the signed-in control panel for names and TLDs. // // One page, a left menu, several panes: Overview, Domain names (with a // per-name detail: DNS records, content & hosting, redirect, transfer, // export), My TLD list (with a per-TLD detail), Wallet, Settings. // // Trust model is unchanged from the old portal: the wallet lives in this // browser; every on-chain change is a transaction signed here; DNS records // are a manifest signed here and stored on Sia via the gateway. What is new // 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=20260917sig"; 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"; const $ = (id) => document.getElementById(id); const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ","); const shortAddr = (a) => (a ? a.replace(/^[^:]+:/, "").slice(0, 8) + "…" + a.slice(-6) : "—"); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // ---------- state ---------- let wallet = null; const H = { names: [], tlds: [], wallet: null, chain: null, loadedAt: 0 }; const dnsCache = new Map(); // name -> { status: "none"|"ok"|"nopointer"|"error", seq, dns, raw } let registryCache = { at: 0, names: [] }; let current = { name: null, entry: null, manifestSeq: 0, rows: [], dirty: false }; let currentTld = null; // ---------- profile breadcrumb (shared with profile-menu.js) ---------- function writeProfile(w) { try { localStorage.setItem("siriusProfile", JSON.stringify({ address: w.address, tokenAddress: w.tokenAddress, source: w.source ?? "seed", signedInAt: Date.now(), })); } catch {} window.dispatchEvent(new Event("siriusProfileChanged")); } function readProfile() { try { return JSON.parse(localStorage.getItem("siriusProfile") || "null"); } catch { return null; } } function clearProfile() { try { localStorage.removeItem("siriusProfile"); } catch {} window.dispatchEvent(new Event("siriusProfileChanged")); } // ---------- session ---------- async function restoreSession() { if (window.siriusWallet) return window.siriusWallet; const S = window.siriusSession; if (!S?.restore) return null; 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); window.siriusWallet = w; return w; } catch { return null; } } async function ensureSignInFlow() { for (let i = 0; i < 80 && typeof window.siriusRenderSignInInline !== "function"; i++) await sleep(50); return typeof window.siriusRenderSignInInline === "function"; } async function paintInlineSignIn(mode) { if (!(await ensureSignInFlow())) return; const hasSaved = (() => { try { return !!localStorage.getItem("bns.wallet.v1"); } catch { return false; } })(); window.siriusRenderSignInInline($("signin-inline"), mode || (hasSaved ? "unlock" : "import")); } function showSignedOut() { wallet = null; window.siriusWallet = null; $("me").classList.add("hidden"); $("signed-out").classList.remove("hidden"); } async function enter(w) { wallet = w; window.siriusWallet = w; $("signed-out").classList.add("hidden"); $("me").classList.remove("hidden"); $("side-addr").textContent = w.address; $("me-addr").textContent = w.address; $("me-taddr").textContent = w.tokenAddress; $("me-info").textContent = `watched addresses: ${w.watchedAddresses.length}`; $("w-watched").textContent = String(w.watchedAddresses.length); // Operator tools are only useful to the operator wallet. $("side-admin").classList.toggle("hidden", w.address !== OPERATOR_ADDRESS); paintSettings(); updateTldPrice(); updateTldButtons(); routeFromHash(); await loadHoldings(); } async function signOut({ forget = false } = {}) { if (wallet?.source === "wc" && wallet.session) { try { await wallet.session.disconnect(); } catch {} } try { await window.siriusSession?.forget?.(); } catch {} if (forget) { try { BNS.BuiltInWallet.forget(); } catch {} try { window.siriusPin?.clear?.(); } catch {} } clearProfile(); showSignedOut(); history.replaceState(null, "", location.pathname); await paintInlineSignIn(forget ? "import" : "unlock"); } window.addEventListener("siriusProfileChanged", async () => { if (readProfile()) { if (window.siriusWallet && !wallet) await enter(window.siriusWallet); } else if (wallet) { try { await window.siriusSession?.forget?.(); } catch {} showSignedOut(); await paintInlineSignIn(); } }); window.addEventListener("storage", async (e) => { if (e.key !== "siriusProfile") return; if (!readProfile() && wallet) { showSignedOut(); await paintInlineSignIn(); } }); (async function bootstrap() { const mode = new URLSearchParams(location.search).get("mode"); if (window.siriusWallet) return enter(window.siriusWallet); const w = await restoreSession(); if (w) { writeProfile(w); return enter(w); } await paintInlineSignIn(mode || undefined); })(); // ---------- navigation ---------- const PANES = ["studio", "domains", "domain", "tlds", "tld", "newtld", "settings"]; function go(pane, arg) { if (!PANES.includes(pane)) pane = "domains"; document.querySelectorAll(".pane[data-pane]").forEach((p) => { p.hidden = p.dataset.pane !== pane; }); const activeItem = pane === "domain" ? "domains" : pane === "tld" ? "tlds" : pane; document.querySelectorAll(".side .item[data-go]").forEach((b) => b.classList.toggle("active", b.dataset.go === activeItem)); const hash = arg ? `#${pane}/${encodeURIComponent(arg)}` : `#${pane}`; if (location.hash !== hash) history.replaceState(null, "", hash); if (pane === "domain" && arg) openDomain(arg); if (pane === "tld" && arg) openTld(arg); if (pane === "tlds") renderTlds(); if (pane === "domains") renderDomains(); if (pane === "studio") renderStudio(); window.scrollTo({ top: 0, behavior: "smooth" }); } function routeFromHash() { const m = /^#([a-z]+)(?:\/(.+))?$/.exec(location.hash || ""); if (!m) return go("domains"); go(m[1], m[2] ? decodeURIComponent(m[2]) : undefined); } window.addEventListener("hashchange", () => { if (wallet) routeFromHash(); }); document.addEventListener("click", (e) => { const el = e.target.closest("[data-go]"); if (!el) return; e.preventDefault(); go(el.dataset.go, el.dataset.arg); }); $("side-signout").addEventListener("click", () => signOut()); $("set-signout").addEventListener("click", () => signOut()); $("set-forget").addEventListener("click", async () => { if (!confirm("Forget the wallet on this device? You will need your recovery phrase to sign in again.")) return; await signOut({ forget: true }); }); $("refresh-btn").addEventListener("click", () => loadHoldings()); document.querySelectorAll("[data-copy]").forEach((b) => b.addEventListener("click", () => { const t = $(b.dataset.copy)?.textContent || ""; navigator.clipboard?.writeText(t); const o = b.textContent; b.textContent = "Copied"; setTimeout(() => { b.textContent = o; }, 1200); })); // ---------- holdings ---------- async function loadHoldings() { const status = $("names-status"); status.className = "status"; status.textContent = "Loading names from the chain…"; try { const scripthashes = wallet.watchedAddresses.map((a) => BNS.addressToScripthash(a)); const r = await fetch(`${API}/api/holdings`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ scripthashes }) }); if (!r.ok) throw new Error(`API ${r.status}: ${(await r.text().catch(() => "")).slice(0, 200)}`); const p = await r.json(); H.names = (p.names || []).map((n) => ({ ...n, label: n.name.split(".")[0] })); H.tlds = p.tlds || []; H.wallet = p.wallet || null; H.chain = p.chain || null; H.loadedAt = Date.now(); // header numbers const setText = (id, v) => { const el = $(id); if (el) el.textContent = v; }; setText("cnt-names", H.names.length); setText("cnt-tlds", H.tlds.length); if (H.wallet?.sats != null) { const s = fmtInt(H.wallet.sats); setText("side-balance", s); setText("w-sats", `${s} sat`); setText("w-utxos", String(H.wallet.utxo_count)); // The header wallet menu (profile-menu.js) shows this on every page. try { localStorage.setItem("siriusWalletInfo", JSON.stringify({ sats: String(H.wallet.sats), utxos: H.wallet.utxo_count, names: H.names.length, tlds: H.tlds.length, at: Date.now() })); } catch {} } if (H.chain?.height != null) setText("stat-height", fmtInt(H.chain.height)); renderAttention(); renderDomains(); renderTlds(); renderStudio(); if (current.name) { const e = H.names.find((n) => n.name === current.name); if (e) { current.entry = e; fillSummary(e); } } warmDnsCache(); } catch (e) { status.className = "status err"; status.textContent = "Could not load names: " + (e.message || e); } } // DNS presence per name, fetched in the background (4 at a time) so the // list can show a badge and Overview can flag names without records. async function fetchDns(name, force = false) { if (!force && dnsCache.has(name)) return dnsCache.get(name); let out; try { const r = await fetch(`${API}/api/records/${encodeURIComponent(name)}`, { cache: "no-store" }); if (r.status === 404) out = { status: "none", seq: 0, dns: {} }; else if (r.status === 409) out = { status: "nopointer", seq: 0, dns: {} }; else if (!r.ok) out = { status: "error", seq: 0, dns: {} }; else { const j = await r.json(); out = { status: "ok", seq: typeof j.seq === "number" ? j.seq : 0, dns: j.dns || {}, raw: j }; } } catch { out = { status: "error", seq: 0, dns: {} }; } dnsCache.set(name, out); return out; } async function warmDnsCache() { const todo = H.names.filter((n) => !dnsCache.has(n.name)).map((n) => n.name); let i = 0; const worker = async () => { while (i < todo.length) { const n = todo[i++]; await fetchDns(n); updateDnsBadge(n); } }; await Promise.all([worker(), worker(), worker(), worker()]); renderAttention(); } function dnsCount(d) { if (!d) return 0; return ["A", "AAAA", "MX", "TXT", "NS", "SRV", "CAA"].reduce((n, k) => n + (Array.isArray(d[k]) ? d[k].length : 0), 0) + (d.CNAME ? 1 : 0); } function updateDnsBadge(name) { const c = dnsCache.get(name); document.querySelectorAll(`[data-dnsbadge="${CSS.escape(name)}"]`).forEach((el) => { const n = c?.status === "ok" ? dnsCount(c.dns) : 0; el.className = "badge " + (n ? "b-ok" : "b-dim"); el.textContent = n ? `DNS · ${n}` : (c?.status === "nopointer" ? "DNS needs s3" : "no DNS"); }); if (current.name === name) fillDnsBadge(); } // ---------- classification helpers ---------- function pointsTo(rec) { if (!rec) return { kind: "none", label: "not pointed", cls: "b-warn" }; if (rec.h) return { kind: "h", label: "site on chain", cls: "b-x" }; if (rec.s3) return { kind: "s3", label: "site on Sia", cls: "b-x" }; if (rec.p) return { kind: "p", label: "reverse proxy", cls: "b-x" }; if (rec.ip) return { kind: "ip", label: "own server", cls: "b-x" }; if (rec.u) return { kind: "u", label: "redirect", cls: "b-x" }; return { kind: "none", label: "not pointed", cls: "b-warn" }; } const siteUrl = (name) => `${API}/bns/${encodeURIComponent(name)}/`; const txUrl = (txid) => `https://chipnet.imaginary.cash/tx/${txid}`; // ---------- attention strip (top of Domain names) ---------- function renderAttention() { const items = []; for (const e of H.names) { const pt = pointsTo(e.records); if (pt.kind === "none") items.push({ e, why: "points nowhere yet", go: "content" }); } const box = $("ov-attention"); if (!items.length) { box.innerHTML = ""; return; } box.innerHTML = `
Needs attention
` + items.slice(0, 5).map(({ e, why, go }) => `
${nameHtml(e)}
${esc(why)}
`).join(""); } // ---------- web-builder (Sirius Studio) ---------- function renderStudio() { const status = $("studio-status"), list = $("studio-list"); if (!status || !list) return; if (!H.names.length) { status.className = "status"; status.innerHTML = `You need a name first. Register one → — then come back here to build its site.`; list.innerHTML = ""; return; } status.className = "status ok"; 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 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" } : rec.u ? { label: "redirect", cls: "b-dim" } : rec.p || rec.ip ? { label: "own server", cls: "b-dim" } : { label: "no site yet", cls: "b-warn" }; return `
${nameHtml(e)} ${esc(st.label)}
${rec.s3 || rec.h ? `View →` : ""} ${rec.s3 === studioFolder ? "Edit in Studio" : "Open in Studio"} →
`; }).join(""); } // ---------- domains list ---------- function nameHtml(e) { return `${esc(e.label)}.${esc(e.tld)}`; } function domainRow(e, { compact = false } = {}) { const pt = pointsTo(e.records); const d = dnsCache.get(e.name); const dn = d?.status === "ok" ? dnsCount(d.dns) : 0; const dnsBadge = `${dn ? `DNS · ${dn}` : (d?.status === "nopointer" ? "DNS needs s3" : (d ? "no DNS" : "DNS …"))}`; const acts = compact ? "" : `
Open →
`; return `
${nameHtml(e)} ${esc(pt.label)} ${dnsBadge}
${acts} ${compact ? "" : `
block ${e.height ? fmtInt(e.height) : "mempool"}·${esc((e.category || "").slice(0, 12))}…
`}
`; } function renderDomains() { const list = $("names-list"), status = $("names-status"); const q = ($("dom-search").value || "").trim().toLowerCase(); const sort = $("dom-sort").value; const tldSel = $("dom-tld"); // TLD filter options (rebuild keeping selection) const tlds = [...new Set(H.names.map((n) => n.tld))].sort(); const keep = tldSel.value; tldSel.innerHTML = `` + tlds.map((t) => ``).join(""); let rows = H.names.filter((n) => (!q || n.name.includes(q)) && (!tldSel.value || n.tld === tldSel.value)); rows.sort((a, b) => sort === "tld" ? (a.tld.localeCompare(b.tld) || a.label.localeCompare(b.label)) : sort === "height" ? ((b.height || 1e12) - (a.height || 1e12)) : a.name.localeCompare(b.name)); $("dom-count").textContent = `${rows.length} of ${H.names.length}`; if (!H.names.length) { status.className = "status"; status.textContent = "No BCNR names found for this wallet. If you just registered, wait a minute for confirmation and refresh."; list.innerHTML = ""; return; } status.className = "status ok"; status.textContent = `You own ${H.names.length} name${H.names.length === 1 ? "" : "s"}.`; list.innerHTML = rows.length ? rows.map((e) => domainRow(e)).join("") : `
Nothing matches that filter.
`; } ["dom-search", "dom-sort", "dom-tld"].forEach((id) => $(id).addEventListener("input", renderDomains)); document.addEventListener("click", (e) => { if (e.target.closest("[data-stop]")) return; const el = e.target.closest("[data-open]"); if (!el) return; e.preventDefault(); pendingSub = el.dataset.sub || "summary"; go("domain", el.dataset.open); }); let pendingSub = "summary"; // ---------- domain detail ---------- function setSub(name) { document.querySelectorAll(".subtabs [data-sub]").forEach((b) => b.classList.toggle("active", b.dataset.sub === name)); document.querySelectorAll('.pane[data-pane="domain"] .sub[data-sub]').forEach((s) => { s.hidden = s.dataset.sub !== name; }); if (name === "export") renderExportPreview(); } document.querySelectorAll(".subtabs [data-sub]").forEach((b) => b.addEventListener("click", () => setSub(b.dataset.sub))); async function openDomain(name) { const entry = H.names.find((n) => n.name === name); if (!entry) { if (!H.names.length && Date.now() - H.loadedAt > 1000) { $("dd-name").textContent = name; $("dd-crumb").textContent = name; return; } $("dd-name").textContent = name; $("dd-crumb").textContent = name; $("dd-kv").innerHTML = `
Status
This wallet does not hold ${esc(name)}.
`; return; } current = { name, entry, manifestSeq: 0, rows: [], dirty: false }; $("dd-name").textContent = name; $("dd-crumb").textContent = name; $("dd-view").href = siteUrl(name); fillSummary(entry); fillContentForm(entry.records || {}); $("rd-url").value = entry.records?.u || ""; $("tr-addr").value = ""; $("tr-ack").checked = false; $("tr-send").disabled = true; setMsg("tr-msg", ""); setMsg("dns-msg", ""); setMsg("ct-msg", ""); setMsg("rd-msg", ""); setSub(pendingSub || "summary"); pendingSub = "summary"; await loadDns(name, true); } function setMsg(id, text, cls = "") { const m = $(id); if (!m) return; m.textContent = text; m.className = "msg " + cls; } function logger(id) { const log = $(id); log.className = "steps-log on"; log.innerHTML = ""; return (t) => { const d = document.createElement("div"); d.textContent = String(t); log.appendChild(d); log.scrollTop = log.scrollHeight; }; } function fillSummary(e) { const kv = [ ["Name", `${esc(e.name)}`], ["TLD", `.${esc(e.tld)}`], ["Owner", `${esc(wallet.address)}`], ["Certificate", `${esc(e.category || "")}`], ["Registered", e.height ? `block ${fmtInt(e.height)} · tx →` : `mempool · tx →`], ["Last update", e.updated_txid ? `${esc(e.updated_txid.slice(0, 16))}… →` : "none since registration"], ["Renewal", "never — names do not expire"], ["Public URLs", `silentmode.st gateway · navigate.st`], ]; $("dd-kv").innerHTML = kv.map(([k, v]) => `
${k}
${v}
`).join(""); const rec = e.records || {}; const pt = pointsTo(rec); const rows = [["Serves", `${esc(pt.label)}`]]; const labels = { h: "Inline HTML", s3: "Sia bucket", p: "Reverse proxy", ip: "Server IPv4", tls: "TLS pin", u: "Redirect" }; for (const k of ["h", "s3", "p", "ip", "tls", "u"]) if (rec[k]) rows.push([labels[k], `${esc(String(rec[k]).slice(0, 120))}`]); if (rows.length === 1) rows.push(["Records", `none — point it somewhere →`]); $("dd-records").innerHTML = rows.map(([k, v]) => `
${k}
${v}
`).join(""); $("dd-records").querySelectorAll("[data-sub-go]").forEach((a) => a.onclick = (ev) => { ev.preventDefault(); setSub(a.dataset.subGo); }); fillDnsBadge(); } function fillDnsBadge() { const c = dnsCache.get(current.name); const n = c?.status === "ok" ? dnsCount(c.dns) : 0; const b = $("dd-dnsbadge"); b.className = "badge " + (n ? "b-ok" : "b-dim"); b.textContent = n ? `DNS · ${n} record${n === 1 ? "" : "s"}` : (c?.status === "nopointer" ? "DNS needs s3" : "no DNS"); const sum = $("dd-dns-summary"); if (!c) { sum.textContent = "Loading…"; return; } if (c.status === "nopointer") { sum.innerHTML = `This name has no s3 record, so there is nowhere to store a DNS manifest yet. Set one under Content & hosting first.`; return; } if (!n) { sum.innerHTML = `No DNS records. Add some →`; sum.querySelector("[data-sub-go]").onclick = (ev) => { ev.preventDefault(); setSub("dns"); }; return; } const d = c.dns; const parts = []; for (const k of ["A", "AAAA", "NS", "TXT"]) if (Array.isArray(d[k]) && d[k].length) parts.push(`${k} ${d[k].map((v) => esc(String(v).slice(0, 48))).join(", ")}`); if (d.CNAME) parts.push(`CNAME ${esc(d.CNAME)}`); if (Array.isArray(d.MX) && d.MX.length) parts.push(`MX ${d.MX.map((m) => `${m.pref} ${esc(m.host)}`).join(", ")}`); if (Array.isArray(d.SRV) && d.SRV.length) parts.push(`SRV ${d.SRV.length}`); if (Array.isArray(d.CAA) && d.CAA.length) parts.push(`CAA ${d.CAA.map((x) => `${x.tag} ${esc(x.value)}`).join(", ")}`); sum.innerHTML = parts.join("
") + `
manifest seq ${c.seq}
`; } // ---------- DNS editor ---------- const DNS_HELP = { A: ["IPv4 address", "1.2.3.4"], AAAA: ["IPv6 address", "2001:db8::1"], CNAME: ["Target host (one CNAME per name)", "target.example.com"], MX: ["Mail host", "mail.example.com"], TXT: ["Text (SPF, DKIM, verification…)", "v=spf1 include:_spf.silentmode.st -all"], NS: ["Nameserver host", "ns1.example.com"], SRV: ["Target host", "sip.example.com"], CAA: ["CA domain or report URL", "letsencrypt.org"], }; function rowsFromManifest(d) { const rows = []; for (const t of ["A", "AAAA", "TXT", "NS"]) for (const v of (Array.isArray(d?.[t]) ? d[t] : [])) rows.push({ type: t, value: String(v) }); if (d?.CNAME) rows.push({ type: "CNAME", value: String(d.CNAME) }); for (const m of (Array.isArray(d?.MX) ? d.MX : [])) rows.push({ type: "MX", value: String(m.host), prio: Number(m.pref) || 0 }); for (const s of (Array.isArray(d?.SRV) ? d.SRV : [])) rows.push({ type: "SRV", value: String(s.target), prio: Number(s.priority) || 0, weight: Number(s.weight) || 0, port: Number(s.port) || 0 }); for (const c of (Array.isArray(d?.CAA) ? d.CAA : [])) rows.push({ type: "CAA", value: String(c.value), tag: c.tag || "issue", flags: Number(c.flags) || 0 }); return rows; } function manifestFromRows(rows) { const d = { A: [], AAAA: [], MX: [], TXT: [], CNAME: null, NS: [] }; const srv = [], caa = []; for (const r of rows) { if (r.type === "MX") d.MX.push({ pref: r.prio || 0, host: r.value }); else if (r.type === "CNAME") d.CNAME = r.value; else if (r.type === "SRV") srv.push({ priority: r.prio || 0, weight: r.weight || 0, port: r.port || 0, target: r.value }); else if (r.type === "CAA") caa.push({ flags: r.flags || 0, tag: r.tag || "issue", value: r.value }); else if (d[r.type]) d[r.type].push(r.value); } if (srv.length) d.SRV = srv; if (caa.length) d.CAA = caa; return d; } function extraOf(r) { if (r.type === "MX") return `priority ${r.prio}`; if (r.type === "SRV") return `prio ${r.prio} · weight ${r.weight} · port ${r.port}`; if (r.type === "CAA") return `${r.tag}${r.flags ? ` · flags ${r.flags}` : ""}`; return ""; } function renderDnsTable() { const tb = $("dns-table").querySelector("tbody"); tb.innerHTML = current.rows.map((r, i) => ` ${esc(r.type)}${esc(r.value)}${esc(extraOf(r))} `).join(""); $("dns-empty").classList.toggle("hidden", current.rows.length > 0); $("dns-seq").textContent = `seq ${current.manifestSeq}${current.dirty ? " · unsaved" : ""}`; tb.querySelectorAll("[data-del]").forEach((b) => b.onclick = () => { current.rows.splice(Number(b.dataset.del), 1); current.dirty = true; renderDnsTable(); }); } async function loadDns(name, force) { const c = await fetchDns(name, force); if (current.name !== name) return; current.manifestSeq = c.seq || 0; current.rows = c.status === "ok" ? rowsFromManifest(c.dns) : []; current.dirty = false; renderDnsTable(); updateDnsBadge(name); const pub = $("dns-publish"); if (c.status === "nopointer") { setMsg("dns-msg", "This name has no s3 record on chain, so the manifest has nowhere to live. Set one under Content & hosting first (any Sia bucket key works).", "err"); pub.disabled = true; } else { pub.disabled = false; } } function updateDnsForm() { const t = $("dns-type").value; document.querySelectorAll(".addrow .opt").forEach((o) => { o.hidden = !o.dataset.for.split(" ").includes(t); }); const [label, ph] = DNS_HELP[t] || ["Value", ""]; $("dns-value-label").textContent = label; $("dns-value").placeholder = ph; $("dns-help").textContent = t === "CNAME" ? "A CNAME replaces any existing CNAME; classic DNS does not allow it next to other records on the same host." : t === "TXT" ? "One string per record. Long DKIM keys are fine — there is no size cap in the manifest." : t === "CAA" ? "Restricts which certificate authorities may issue for this name in classic DNS." : ""; } $("dns-type").addEventListener("change", updateDnsForm); updateDnsForm(); $("dns-add").addEventListener("click", () => { const type = $("dns-type").value; const value = $("dns-value").value.trim(); if (!value) { $("dns-value").focus(); return; } if (type === "A" && !/^\d{1,3}(\.\d{1,3}){3}$/.test(value)) return setMsg("dns-msg", "That is not an IPv4 address.", "err"); if (type === "AAAA" && !/^[0-9a-f:]+$/i.test(value)) return setMsg("dns-msg", "That is not an IPv6 address.", "err"); const row = { type, value }; if (type === "MX" || type === "SRV") row.prio = Number($("dns-prio").value) || 0; if (type === "SRV") { row.weight = Number($("dns-weight").value) || 0; row.port = Number($("dns-port").value) || 0; } if (type === "CAA") { row.tag = $("dns-tag").value; row.flags = 0; } if (type === "CNAME") current.rows = current.rows.filter((r) => r.type !== "CNAME"); current.rows.push(row); current.dirty = true; $("dns-value").value = ""; $("dns-prio").value = ""; $("dns-weight").value = ""; $("dns-port").value = ""; setMsg("dns-msg", ""); renderDnsTable(); }); $("dns-value").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); $("dns-add").click(); } }); $("dns-reload").addEventListener("click", () => loadDns(current.name, true)); $("dns-publish").addEventListener("click", async () => { if (!current.name || !wallet) return; if (wallet.source === "wc") return setMsg("dns-msg", "External-wallet signing (WizardConnect) for records manifests lands in the next drop — please sign in with the built-in wallet.", "err"); const push = logger("dns-log"); const btn = $("dns-publish"); btn.disabled = true; btn.textContent = "Signing…"; setMsg("dns-msg", ""); try { const manifest = { v: 1, name: current.name, seq: (current.manifestSeq || 0) + 1, updated_at: new Date().toISOString(), dns: manifestFromRows(current.rows) }; push(`composed manifest · seq ${manifest.seq} · ${current.rows.length} record${current.rows.length === 1 ? "" : "s"}`); const signed = await BNS.signRecordsManifest(wallet, manifest); push("signed with wallet key"); const r = await fetch(`${API}/api/records/${encodeURIComponent(current.name)}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(signed) }); const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(`API ${r.status}: ${j.error || "unknown error"}`); push(`published · ${j.bytes || "?"} bytes at ${j.sia_key}`); current.manifestSeq = manifest.seq; current.dirty = false; dnsCache.set(current.name, { status: "ok", seq: manifest.seq, dns: manifest.dns, raw: manifest }); renderDnsTable(); updateDnsBadge(current.name); renderAttention(); setMsg("dns-msg", `Published manifest seq ${manifest.seq}. Resolvers pick it up on their next fetch (about 30 s at the gateway).`, "ok"); } catch (e) { setMsg("dns-msg", "Failed: " + (e.message || e), "err"); push("error: " + (e.message || e)); } finally { btn.disabled = false; btn.textContent = "Sign & publish →"; } }); // ---------- content & hosting (on-chain records) ---------- const CT_KEYS = ["h", "s3", "p", "ip", "tls"]; function fillContentForm(rec) { for (const k of CT_KEYS) $("ct-" + k).value = rec[k] || ""; updateBudget(); } function collectContent() { const r = {}; for (const k of CT_KEYS) { const v = $("ct-" + k).value.trim(); if (v) r[k] = v; } const u = ($("rd-url").value || "").trim(); if (u) r.u = u; return r; } function updateBudget() { if (!current.name) return; try { const max = BNS.payloadBudget(current.name, "UPD"); const used = new TextEncoder().encode(JSON.stringify(collectContent())).length; $("ct-budget-max").textContent = max; $("ct-budget-used").textContent = used; $("ct-budget").classList.toggle("over", used > max); $("ct-save").disabled = used > max; } catch {} } CT_KEYS.forEach((k) => $("ct-" + k).addEventListener("input", updateBudget)); $("rd-url").addEventListener("input", updateBudget); $("ct-reset").addEventListener("click", () => fillContentForm(current.entry?.records || {})); async function publishRecords(records, logId, msgId, btnId, doneText) { if (!current.name || !wallet) return false; const push = logger(logId); const btn = $(btnId); const orig = btn.textContent; btn.disabled = true; btn.textContent = "Signing…"; setMsg(msgId, ""); let el = null; try { el = await BNS.connect(); push("connected to chipnet"); 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) }); push("broadcast: " + (res.txid || res.txId || "(no txid)")); if (current.entry) { current.entry.records = records; current.entry.updated_txid = res.txid || res.txId || current.entry.updated_txid; } fillSummary(current.entry); renderDomains(); renderAttention(); renderStudio(); setMsg(msgId, doneText, "ok"); return true; } catch (e) { setMsg(msgId, "Failed: " + (e.message || e), "err"); push("error: " + (e.message || e)); return false; } finally { btn.disabled = false; btn.textContent = orig; try { el?.close?.(); } catch {} } } $("ct-save").addEventListener("click", () => publishRecords(collectContent(), "ct-log", "ct-msg", "ct-save", "Saved. Records refresh on the chain within a block.")); $("rd-save").addEventListener("click", () => { const u = $("rd-url").value.trim(); if (!/^https?:\/\//i.test(u)) return setMsg("rd-msg", "Enter a full URL starting with http:// or https://", "err"); publishRecords(collectContent(), "rd-log", "rd-msg", "rd-save", "Redirect saved. Resolvers forward the name within a block."); }); $("rd-clear").addEventListener("click", () => { $("rd-url").value = ""; publishRecords(collectContent(), "rd-log", "rd-msg", "rd-clear", "Redirect removed."); }); // ---------- transfer ---------- function validAddress(a) { try { BNS.addressToLockingBytecode(a); return true; } catch { return false; } } function updateTransferBtn() { const a = $("tr-addr").value.trim(); $("tr-send").disabled = !( $("tr-ack").checked && validAddress(a) && a !== wallet?.address ); } $("tr-addr").addEventListener("input", updateTransferBtn); $("tr-ack").addEventListener("change", updateTransferBtn); $("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 (!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"); 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(""); const cert = utxos.find((u) => u.token?.category === current.entry.category && u.token?.nft?.commitment === commitment); if (!cert) throw new Error("certificate UTXO not found in this wallet (already moved?)"); push("found certificate · building transfer"); const built = BNS.buildUpdateTx({ name: current.name, ownerAddress: wallet.address, ownerTokenAddress: BNS.toTokenAddress(to), records: current.entry.records || {}, certificateUtxo: cert, utxos: utxos.filter((u) => !u.token), addressToLockingBytecode: BNS.addressToLockingBytecode, }); const signed = BNS.signTransaction(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h)); push("signed · broadcasting"); const txid = await BNS.broadcast(el, signed.hex); push("broadcast: " + txid); setMsg("tr-msg", `Transferred. ${current.name} now belongs to ${to}. It disappears from this list after the next refresh.`, "ok"); btn.textContent = "Transferred ✓"; setTimeout(() => loadHoldings(), 4000); } catch (e) { setMsg("tr-msg", "Failed: " + (e.message || e), "err"); push("error: " + (e.message || e)); btn.disabled = false; btn.textContent = "Transfer name →"; } finally { try { el?.close?.(); } catch {} } }); // ---------- export ---------- function zoneText() { const e = current.entry; if (!e) return ""; const c = dnsCache.get(e.name); const d = c?.status === "ok" ? c.dns : {}; const L = [`; ${e.name} — exported from Sirius.X ${new Date().toISOString()}`, `$ORIGIN ${e.name}.`, `$TTL 300`, ``]; const rec = e.records || {}; L.push(`; on-chain records (authoritative for BCNR resolvers)`); for (const k of ["h", "s3", "p", "ip", "tls", "u"]) if (rec[k]) L.push(`; ${k} = ${String(rec[k]).replace(/\n/g, " ")}`); if (rec.ip && !(Array.isArray(d.A) && d.A.length)) L.push(`@\tIN\tA\t${rec.ip}`); L.push(``, `; signed DNS manifest${c?.seq ? ` (seq ${c.seq})` : ""}`); for (const v of d.A || []) L.push(`@\tIN\tA\t${v}`); for (const v of d.AAAA || []) L.push(`@\tIN\tAAAA\t${v}`); if (d.CNAME) L.push(`@\tIN\tCNAME\t${d.CNAME}.`); for (const m of d.MX || []) L.push(`@\tIN\tMX\t${m.pref}\t${m.host}.`); for (const v of d.NS || []) L.push(`@\tIN\tNS\t${v}.`); for (const v of d.TXT || []) L.push(`@\tIN\tTXT\t"${String(v).replace(/"/g, '\\"')}"`); for (const s of d.SRV || []) L.push(`@\tIN\tSRV\t${s.priority} ${s.weight} ${s.port}\t${s.target}.`); for (const x of d.CAA || []) L.push(`@\tIN\tCAA\t${x.flags || 0} ${x.tag} "${x.value}"`); return L.join("\n") + "\n"; } function renderExportPreview() { $("ex-preview").textContent = zoneText(); } function download(filename, text, type = "text/plain") { const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([text], { type })); a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); } $("ex-zone").addEventListener("click", () => download(`${current.name}.zone.txt`, zoneText())); $("ex-json").addEventListener("click", () => { const c = dnsCache.get(current.name); download(`${current.name}.json`, JSON.stringify({ name: current.name, chain: current.entry, dns: c?.raw || null, exported_at: new Date().toISOString() }, null, 2), "application/json"); }); $("ex-copy").addEventListener("click", () => { const c = dnsCache.get(current.name); navigator.clipboard?.writeText(JSON.stringify(c?.raw || { dns: {} }, null, 2)); }); // ---------- TLDs ---------- function isTldHidden(records) { const h = records?.hidden; return h === 1 || h === true || h === "1"; } function tldPriceOf(records) { const p = records?.price; return typeof p === "number" && Number.isFinite(p) && p >= 0 ? p : null; } const fmtUsd = (n) => (window.siriusPricing?.formatUsd || ((x) => "$" + x))(n); function renderTlds() { const status = $("tlds-status"), list = $("tlds-list"); if (!H.tlds.length) { status.className = "status"; status.innerHTML = `This wallet does not hold any TLD certificates yet. Register one → — every name registered under it earns you 90% of the service fee.`; list.innerHTML = ""; return; } status.className = "status ok"; status.textContent = `You own ${H.tlds.length} TLD${H.tlds.length === 1 ? "" : "s"}.`; list.innerHTML = H.tlds.map((t) => { const rec = t.records || {}; const off = isTldHidden(rec); const price = tldPriceOf(rec); const under = H.names.filter((n) => n.tld === t.tld).length; return `
.${esc(t.tld)} ${off ? "off · private" : "on · public"} ${price != null ? `${esc(fmtUsd(price))} / name` : "tier pricing"}
${under} of your names under it·policy ${esc(rec.policy || "open")}
`; }).join(""); } async function registry() { if (Date.now() - registryCache.at < 60000) return registryCache.names; try { const r = await fetch(`${API}/api/registry`, { cache: "no-store" }); const j = await r.json(); registryCache = { at: Date.now(), names: j.names || [] }; } catch { registryCache.at = Date.now(); } return registryCache.names; } async function openTld(label) { const t = H.tlds.find((x) => x.tld === label); currentTld = t || null; $("td-name").textContent = "." + label; $("td-crumb").textContent = "." + label; $("td-suffix").textContent = "." + label; setMsg("td-msg", ""); if (!t) { $("td-kv").innerHTML = `
Status
This wallet does not hold .${esc(label)}.
`; return; } const rec = t.records || {}; const off = isTldHidden(rec); const price = tldPriceOf(rec); const st = $("td-state"); st.className = "badge " + (off ? "b-off" : "b-ok"); st.textContent = off ? "off · private" : "on · public"; $("td-price").textContent = price != null ? `${fmtUsd(price)} / name` : "tier pricing"; $("td-toggle").textContent = off ? "Switch on" : "Switch off"; let pub = null; try { const j = await (await fetch(TLD_API, { cache: "no-store" })).json(); pub = (j.tlds || []).find((x) => x.tld === label) || null; } catch {} const kv = [ ["Certificate", `${esc(t.category)}`], ["Owner", `${esc(t.owner || wallet.address)}`], ["Registered", t.reg_height ? `block ${fmtInt(t.reg_height)}` : "mempool"], ["Last policy update", t.updated_txid ? `${esc(t.updated_txid.slice(0, 16))}… →` : "none"], ["Names registered", pub ? String(pub.registered_count) : "—"], ["Price", price != null ? `${esc(fmtUsd(price))} per name (you receive 90%)` : "length tiers ($0.10 – $5)"], ["Listing", off ? "off — private: only you can register names" : "on — anyone can register names"], ["Policy", esc(rec.policy || "open") + (rec.min_len ? ` · min length ${rec.min_len}` : "") + (rec.reserved ? ` · reserved: ${esc(rec.reserved)}` : "")], ]; $("td-kv").innerHTML = kv.map(([k, v]) => `
${k}
${v}
`).join(""); const names = (await registry()).filter((n) => n.tld === label); $("td-names").innerHTML = names.length ? names.map((n) => `
${esc(n.name)}${H.names.some((m) => m.name === n.name) ? 'yours' : ""}
Open →
block ${n.height ? fmtInt(n.height) : "mempool"}
`).join("") : `
No names registered under .${esc(label)} yet.
`; } $("td-register").addEventListener("click", () => { if (!currentTld) return; const input = $("td-label"); const label = String(input.value || "").toLowerCase().replace(/[^a-z0-9-]/g, ""); input.value = label; if (!label) return input.focus(); if (typeof window.siriusRegisterName === "function") window.siriusRegisterName(`${label}.${currentTld.tld}`); }); $("td-label").addEventListener("keydown", (e) => { if (e.key === "Enter") $("td-register").click(); }); $("td-toggle").addEventListener("click", async () => { if (!currentTld || !wallet) return; if (wallet.source === "wc") return setMsg("td-msg", "External-wallet (WizardConnect) TUPD lands in the next drop — please sign in with the built-in wallet.", "err"); const rec = { ...(currentTld.records || {}) }; const turningOff = !isTldHidden(rec); if (turningOff) rec.hidden = 1; else delete rec.hidden; const btn = $("td-toggle"); btn.disabled = true; btn.textContent = "Signing…"; let el = null; try { el = await BNS.connect(); const res = await BNS.setTldRecordsWithBuiltInWallet(el, { wallet, tld: currentTld.tld, records: rec, category: currentTld.category, onProgress: () => {} }); currentTld.records = rec; setMsg("td-msg", `${turningOff ? "Off" : "On"} — broadcast ${(res.txid || "").slice(0, 16)}…. The public registry picks it up within a minute.`, "ok"); openTld(currentTld.tld); renderTlds(); } catch (e) { setMsg("td-msg", "Failed: " + (e.message || e), "err"); } finally { btn.disabled = false; try { el?.close?.(); } catch {} } }); $("td-policy").addEventListener("click", () => { if (currentTld) openTldEditor(currentTld.tld, currentTld.category, currentTld.records || {}); }); // ---------- TLD policy editor (TUPD) ---------- const TLD_POLICY_FIELDS = [ { key: "price", id: "tld-ed-price", parse: parseUsdOrNull }, { key: "hidden", id: "tld-ed-hidden", parse: (v) => (String(v) === "1" ? 1 : null) }, { key: "min_len", id: "tld-ed-minlen", parse: parseIntOrNull }, { key: "fee_bps", id: "tld-ed-feebps", parse: parseIntOrNull }, { key: "policy", id: "tld-ed-policy", parse: trimOrNull }, { key: "reserved", id: "tld-ed-reserved", parse: trimOrNull }, { key: "renewal_period", id: "tld-ed-renewal", parse: parseIntOrNull }, { key: "renewal_fee", id: "tld-ed-renewalfee", parse: parseIntOrNull }, { key: "site", id: "tld-ed-site", parse: trimOrNull }, ]; function parseIntOrNull(v) { v = String(v).trim(); if (!v) return null; const n = Number(v); return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null; } function parseUsdOrNull(v) { v = String(v).trim().replace(/^\$/, ""); if (!v) return null; const n = Number(v); return Number.isFinite(n) && n >= 0 ? Math.round(n * 100) / 100 : null; } function trimOrNull(v) { v = String(v).trim(); return v || null; } let tldEditor = { tld: null, category: null }; function openTldEditor(tld, category, records) { tldEditor = { tld, category }; $("tld-ed-name").textContent = "." + tld; for (const f of TLD_POLICY_FIELDS) { const cur = records?.[f.key]; $(f.id).value = cur == null ? "" : String(cur); } setMsg("tld-ed-msg", ""); $("tld-ed-log").className = "steps-log"; $("tld-ed-log").innerHTML = ""; $("tld-ed-save").disabled = false; $("tld-ed-save").textContent = "Save policy →"; updateTldBudget(); $("tld-editor").classList.add("open"); } function closeTldEditor() { $("tld-editor").classList.remove("open"); tldEditor = { tld: null, category: null }; } $("tld-ed-close").addEventListener("click", closeTldEditor); $("tld-ed-cancel").addEventListener("click", closeTldEditor); $("tld-editor").addEventListener("click", (e) => { if (e.target === $("tld-editor")) closeTldEditor(); }); function collectTldRecords() { const r = {}; for (const f of TLD_POLICY_FIELDS) { const v = f.parse($(f.id).value); if (v !== null && v !== undefined && v !== "") r[f.key] = v; } return r; } function updateTldBudget() { if (!tldEditor.tld) return; try { const max = BNS.payloadBudget(tldEditor.tld, "TUPD"); const used = new TextEncoder().encode(JSON.stringify(collectTldRecords())).length; $("tld-ed-budget-max").textContent = max; $("tld-ed-budget-used").textContent = used; $("tld-ed-budget").classList.toggle("over", used > max); $("tld-ed-save").disabled = used > max; } catch {} } for (const f of TLD_POLICY_FIELDS) $(f.id).addEventListener("input", updateTldBudget); $("tld-ed-save").addEventListener("click", async () => { if (!tldEditor.tld || !tldEditor.category || !wallet) return; if (wallet.source === "wc") return setMsg("tld-ed-msg", "External-wallet (WizardConnect) TUPD lands in the next drop — please sign in with the built-in wallet.", "err"); const records = collectTldRecords(); const push = logger("tld-ed-log"); $("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"); 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; setMsg("tld-ed-msg", "Policy saved. It reflects in registrations under this TLD within a block.", "ok"); $("tld-ed-save").textContent = "Saved ✓"; renderTlds(); if (currentTld?.tld === tldEditor.tld) openTld(tldEditor.tld); setTimeout(closeTldEditor, 1200); } catch (e) { setMsg("tld-ed-msg", "Failed: " + (e.message || e), "err"); push("error: " + (e.message || e)); $("tld-ed-save").disabled = false; $("tld-ed-save").textContent = "Save policy →"; } finally { try { el?.close?.(); } catch {} } }); // ---------- new TLD ---------- const CHIPNET_SATS_PER_USD = 250_000; function usdToSats(usd) { const n = Number(String(usd).replace(/[^\d.]/g, "")) || 0; return Math.max(0, Math.round(n * CHIPNET_SATS_PER_USD)); } function updateTldPrice() { const sats = usdToSats($("tld-usd").value.trim() || "0"); $("tld-price").innerHTML = `= ${sats.toLocaleString()} sats · rate ${CHIPNET_SATS_PER_USD.toLocaleString()} sats/USD (chipnet)`; $("tld-submit").textContent = `Register (~${sats.toLocaleString()} sats)`; } const validateTldLabel = (l) => /^[a-z0-9-]{1,16}$/.test(l); function updateTldButtons() { const ok = validateTldLabel($("tld-label").value.trim().toLowerCase()); $("tld-check").disabled = !ok || !wallet; $("tld-submit").disabled = !ok || !wallet; } $("tld-label").addEventListener("input", () => { $("tld-label").value = $("tld-label").value.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 16); updateTldButtons(); }); $("tld-usd").addEventListener("input", updateTldPrice); $("tld-check").addEventListener("click", async () => { const label = $("tld-label").value.trim(); const msg = $("tld-msg"); msg.textContent = ""; try { const j = await (await fetch(TLD_API)).json(); const taken = (j.tlds || []).some((t) => (t.tld || t) === label); msg.textContent = taken ? `❌ .${label} is already registered` : `✅ .${label} is available`; msg.style.color = taken ? "var(--taken)" : "var(--ok)"; } catch (e) { msg.textContent = "could not check: " + (e.message || e); msg.style.color = "var(--warn)"; } }); $("tld-submit").addEventListener("click", async () => { if (!wallet) return; const label = $("tld-label").value.trim(); if (!validateTldLabel(label)) return; const sats = BigInt(usdToSats($("tld-usd").value.trim() || "0")); const msg = $("tld-msg"); msg.textContent = ""; const log = $("tld-log"); log.style.display = "block"; log.innerHTML = ""; const push = (t) => { const d = document.createElement("div"); d.textContent = "› " + t; log.appendChild(d); log.scrollTop = log.scrollHeight; }; $("tld-submit").disabled = true; $("tld-submit").textContent = "Signing…"; let el = null; try { el = await BNS.connect(); push("connected to chipnet"); 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" ? await BNS.registerTldWithExternalWallet(el, { session: wallet.session, ...args }) : await BNS.registerTldWithBuiltInWallet(el, { wallet, ...args }); push("broadcast: " + (res.txid || res.txId || "(no txid)")); msg.style.color = "var(--ok)"; msg.textContent = `✅ .${label} minted. Category ${(res.category || "").slice(0, 16)}…`; $("tld-submit").textContent = "Registered ✓"; setTimeout(() => loadHoldings(), 2500); } 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 ${esc(wallet.address)} with chipnet coins first — free from tbch.googol.cash.` : "Failed: " + esc(raw); push("error: " + raw); $("tld-submit").disabled = false; updateTldPrice(); } finally { try { el?.close?.(); } catch {} } }); updateTldPrice(); // ---------- settings ---------- function paintSettings() { const S = window.siriusSession, P = window.siriusPin; $("set-stay").checked = !!S?.isEnabled?.(); $("set-pinpay").checked = !!S?.requirePinForPayments?.(); const hasPin = !!P?.hasPin?.(); $("set-pinstate").textContent = hasPin ? "A PIN is set on this device." : "No PIN set."; $("set-clearpin").disabled = !hasPin; $("set-setpin").disabled = !wallet?.mnemonic; } $("set-stay").addEventListener("change", async (e) => { const S = window.siriusSession; if (!S) return; S.setEnabled(e.target.checked); if (e.target.checked && wallet?.mnemonic) await S.remember(wallet.mnemonic, { accountPath: wallet.accountPath }); setMsg("set-msg", e.target.checked ? "This device stays signed in." : "You will be asked to unlock on your next visit.", "ok"); }); $("set-pinpay").addEventListener("change", (e) => { window.siriusSession?.setRequirePinForPayments(e.target.checked); setMsg("set-msg", e.target.checked ? "Payments will ask for your PIN. Set one below if you have none." : "Payments approve with one click.", "ok"); }); $("set-setpin").addEventListener("click", () => { $("pin-1").value = ""; $("pin-2").value = ""; setMsg("pin-msg", ""); $("pin-modal").classList.add("open"); $("pin-1").focus(); }); const closePin = () => $("pin-modal").classList.remove("open"); $("pin-close").addEventListener("click", closePin); $("pin-cancel").addEventListener("click", closePin); $("pin-modal").addEventListener("click", (e) => { if (e.target === $("pin-modal")) closePin(); }); $("pin-save").addEventListener("click", async () => { const a = $("pin-1").value, b = $("pin-2").value; if (!/^\d{4,6}$/.test(a)) return setMsg("pin-msg", "PIN must be 4–6 digits", "err"); if (a !== b) return setMsg("pin-msg", "PINs do not match", "err"); try { await window.siriusPin.savePinBlob(wallet.mnemonic, a); closePin(); paintSettings(); setMsg("set-msg", "PIN saved.", "ok"); } catch (e) { setMsg("pin-msg", e.message || String(e), "err"); } }); $("set-clearpin").addEventListener("click", () => { window.siriusPin?.clear?.(); paintSettings(); setMsg("set-msg", "PIN removed.", "ok"); });