sirius/js/register-flow.js
Local Dev 5e0c971c23 feat(sirius-x): inline wallet dropdown — no navigation, no portal detour
Wallet dropdown items (New / Import / WizardConnect) were navigating
to portal.html?mode=X and asking the user to click again on arrival.
That is 'the sign in landing page which is not functioning' from the
user's perspective — a whole redirect for one form.

Now every dropdown item opens the mint modal (register-flow.js) inline
at the matching step, on whatever page the user is on:
  New         -> stepCreate  ('Create your wallet' — password + generate)
  Import      -> stepImport  ('Import a recovery phrase' — textarea)
  WizardConnect -> stepExternal ('Open your wallet' — QR/URI)

register-flow.js grew a signInOnly mode: state.name is null, the step
Back buttons close instead of going to a wallet-choice step there is
no context for, and on wallet-loaded the flow writes siriusProfile
and shows a 'Signed in' confirmation instead of Fund -> Confirm -> Mint.

WC signInOnly keeps the session alive (state.session) so a later
record edit can reuse it without a fresh QR handshake.

profile-menu.js: menu items became <a data-action='new|import|wc'>
and the click handler lazy-loads register-flow.js on demand — the
docs/brand/theseus pages don't ship it in their initial payload, so
their nav pill loads it the first time a wallet button is clicked and
caches it for subsequent opens. Falls back to portal.html?mode=X if
the module can't load. Cache-buster bumped so cached copies pick up
the new behavior.

Importmap for @bitauth/libauth added to docs/, brand/, theseus/ so
the bundle's bare specifier resolves when register-flow.js is
lazy-loaded from those pages.

Verified end-to-end on the live docs page: all three dropdown items
open the modal inline at the correct step, no console errors, no
navigation.
2026-09-07 23:52:12 +02:00

617 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Reusable name-registration mint flow — the modal + wallet setup + on-chain
// mint sequence that used to live inside register.html. Any Sirius.X page can
// include this module and call `window.siriusRegisterName(fullName)` to open
// the flow for a specific `<label>.<tld>`. The landing's search cards call
// it directly; a legacy register.html redirect keeps old links working.
//
// 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=20260907tldext";
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c]));
const sats = (v) => Number(v).toLocaleString("en-US");
// ---------- inject CSS + modal HTML ----------
// The style block is a straight copy of register.html's modal/sheet styling
// — kept scoped by prefixing every selector with #sirius-register-modal so it
// cannot collide with the host page's own .modal / .sheet classes.
(function injectAssets() {
if (document.getElementById("sirius-register-flow-css")) return;
const css = document.createElement("style");
css.id = "sirius-register-flow-css";
css.textContent = `
#sirius-register-modal{position:fixed;inset:0;background:rgba(6,9,14,.82);backdrop-filter:blur(3px);
display:none;align-items:flex-start;justify-content:center;padding:3vh 1rem;overflow-y:auto;z-index:100}
#sirius-register-modal.open{display:flex}
#sirius-register-modal .sheet{background:var(--panel,#141a24);border:1px solid var(--line,rgba(255,255,255,.09));
border-radius:16px;max-width:620px;width:100%;padding:26px 28px 28px;color:var(--ink,#e7eaf1)}
#sirius-register-modal .sheet h3{margin:0 0 .2rem;font-size:1.25rem}
#sirius-register-modal .sheet .sub{color:var(--mut,#8b98a9);font-size:14px;margin:0 0 1.2rem}
#sirius-register-modal .sheet label{display:block;font-size:13px;color:var(--mut,#8b98a9);margin:14px 0 5px}
#sirius-register-modal .sheet input[type=text],
#sirius-register-modal .sheet input[type=password],
#sirius-register-modal .sheet textarea,
#sirius-register-modal .sheet select{
width:100%;padding:11px 13px;border-radius:10px;border:1px solid #ffffff22;background:#0e131b;
color:var(--ink,#e7eaf1);font-size:14px;font-family:inherit;outline:none}
#sirius-register-modal .sheet textarea{min-height:76px;resize:vertical;font-family:ui-monospace,monospace;font-size:13px}
#sirius-register-modal .sheet input:focus,
#sirius-register-modal .sheet textarea:focus{border-color:#4b7bec}
#sirius-register-modal .row{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}
#sirius-register-modal .row.end{justify-content:flex-end}
#sirius-register-modal .x{position:absolute;top:14px;right:18px;background:none;border:none;color:var(--dim,#5e6678);font-size:22px;cursor:pointer}
#sirius-register-modal .sheetwrap{position:relative;width:100%;max-width:620px}
#sirius-register-modal .phrase{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}
#sirius-register-modal .phrase span{background:#0e131b;border:1px solid var(--line,rgba(255,255,255,.09));
border-radius:8px;padding:8px 10px;font-family:ui-monospace,monospace;font-size:13px}
#sirius-register-modal .phrase span b{color:var(--dim,#5e6678);font-weight:400;margin-right:6px;font-size:11px}
#sirius-register-modal .addr{display:flex;gap:8px;align-items:center;margin-top:8px}
#sirius-register-modal .addr code{flex:1;word-break:break-all;background:#0e131b;
border:1px solid var(--line,rgba(255,255,255,.09));border-radius:8px;padding:9px 11px;
font-family:ui-monospace,monospace;font-size:12.5px}
#sirius-register-modal table.price{width:100%;border-collapse:collapse;margin-top:12px;font-size:14px}
#sirius-register-modal table.price td{padding:7px 0;border-bottom:1px solid var(--line,rgba(255,255,255,.09));color:var(--mut,#8b98a9)}
#sirius-register-modal table.price td:last-child{text-align:right;font-family:ui-monospace,monospace;color:var(--ink,#e7eaf1)}
#sirius-register-modal table.price tr.total td{border-bottom:none;color:var(--ink,#e7eaf1);font-weight:600;padding-top:12px}
#sirius-register-modal table.price tr.total td:last-child{color:var(--acid,#d6ff3d)}
#sirius-register-modal table.price td small{display:block;color:var(--dim,#5e6678);font-size:11.5px}
#sirius-register-modal .steps-log{margin-top:14px;font-family:ui-monospace,monospace;font-size:12.5px;
color:var(--mut,#8b98a9);background:#0e131b;border:1px solid var(--line,rgba(255,255,255,.09));
border-radius:10px;padding:12px 14px;max-height:180px;overflow-y:auto}
#sirius-register-modal .steps-log div::before{content:" ";color:var(--acid,#d6ff3d)}
#sirius-register-modal .err{color:var(--taken,#f6768a);font-size:13.5px;margin-top:12px}
#sirius-register-modal .ok{color:var(--ok,#4fd1a5)}
#sirius-register-modal .pill{display:inline-block;font-size:11px;padding:2px 8px;border-radius:999px;
background:rgba(214,255,61,.13);color:var(--acid,#d6ff3d);margin-left:8px;vertical-align:middle}
#sirius-register-modal .pill.soon{background:rgba(255,255,255,.07);color:var(--dim,#5e6678)}
#sirius-register-modal .wopt{display:flex;gap:14px;align-items:flex-start;background:var(--panel2,#18202c);
border:1px solid var(--line,rgba(255,255,255,.09));border-radius:12px;padding:14px 16px;cursor:pointer;
text-align:left;width:100%;color:inherit;font:inherit;margin-top:10px}
#sirius-register-modal .wopt:hover{border-color:#4b7bec}
#sirius-register-modal .wopt:disabled{opacity:.45;cursor:not-allowed}
#sirius-register-modal .wopt .ic{font-size:22px;line-height:1}
#sirius-register-modal .wopt b{display:block;font-size:14.5px}
#sirius-register-modal .wopt span{color:var(--mut,#8b98a9);font-size:13px}
#sirius-register-modal .chk{display:flex;gap:9px;align-items:flex-start;margin-top:14px;font-size:13.5px;color:var(--mut,#8b98a9)}
#sirius-register-modal .chk input{margin-top:3px}
#sirius-register-modal .btn{display:inline-block;text-decoration:none;padding:10px 18px;border-radius:10px;
background:#4b7bec;color:#fff;font-size:14px;border:none;cursor:pointer;font-family:inherit}
#sirius-register-modal .btn.ghost{background:transparent;border:1px solid var(--line,rgba(255,255,255,.09));color:var(--ink,#e7eaf1)}
#sirius-register-modal .btn.acid{background:var(--acid,#d6ff3d);color:#0b0e14;font-weight:600}
#sirius-register-modal .btn:hover{filter:brightness(1.12)}
#sirius-register-modal .btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
#sirius-register-modal .mono{background:#0e131b;border:1px solid var(--line,rgba(255,255,255,.09));
border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px}
#sirius-register-modal .muted{color:var(--dim,#5e6678);font-size:13px}
#sirius-register-modal .note{background:var(--panel,#141a24);border:1px solid var(--line,rgba(255,255,255,.09));
border-left:3px solid var(--acid,#d6ff3d);border-radius:10px;padding:14px 18px;
color:var(--mut,#8b98a9);font-size:14px;margin-top:1.4rem}
#sirius-register-modal .note b{color:var(--ink,#e7eaf1)}
#sirius-register-modal .note.warn{border-left-color:var(--warn,#ffc75f)}
`;
document.head.appendChild(css);
const modal = document.createElement("div");
modal.id = "sirius-register-modal";
modal.innerHTML = `
<div class="sheetwrap">
<button class="x" id="sirius-reg-close" title="close">×</button>
<div class="sheet" id="sirius-reg-sheet"></div>
</div>
`;
document.body.appendChild(modal);
})();
// ---------- flow state ----------
// signInOnly: when true, the flow is used for wallet onboarding from the nav
// dropdown — no name to mint, so instead of walking through Fund/Confirm/
// Register we stop at 'wallet is ready', write siriusProfile, and close.
const state = { name: null, signInOnly: false, wallet: null, session: null, client: null, quote: null, result: null };
const modal = $("sirius-register-modal");
const sheet = $("sirius-reg-sheet");
function open() { modal.classList.add("open"); }
function close() {
modal.classList.remove("open");
if (state.client) { try { state.client.close(); } catch {} state.client = null; }
// Keep the WC session alive when signing in — later record edits or TLD
// mints will need it; only tear it down for the mint flow.
if (state.session && !state.signInOnly) { try { state.session.disconnect(); } catch {} state.session = null; }
state.signInOnly = false;
}
// Persist wallet identity so profile-menu.js and any other tab see the
// signed-in state immediately (same as portal.html's writeProfile).
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"));
}
// A wallet was just loaded/created/connected. If we are in sign-in-only
// mode, mark siriusProfile and close the modal here; otherwise return
// false so the caller keeps going to Fund → Confirm → Register.
function finishSignIn(w) {
writeProfile({ address: w.address, tokenAddress: w.tokenAddress, source: w.source ?? "seed" });
if (state.signInOnly) {
render(`<h3><span class="ok">✓</span> Signed in</h3>
<p class="sub">You are signed in as <span class="mono">${esc(w.address)}</span>. Names you own
will appear in the wallet dropdown; the same wallet signs record edits and new registrations.</p>
<div class="row end"><button class="btn acid" id="fin">Done</button></div>`);
$("fin").onclick = close;
return true;
}
return false;
}
$("sirius-reg-close").onclick = close;
modal.addEventListener("click", (e) => { if (e.target === modal) close(); });
function render(html) { sheet.innerHTML = html; }
function err(e) {
sheet.querySelectorAll(".err").forEach((n) => n.remove());
const box = document.createElement("div");
box.className = "err";
box.textContent = e?.message ?? String(e);
sheet.appendChild(box);
}
async function client() {
if (!state.client) state.client = await BNS.connect();
return state.client;
}
function startFlow(name) {
state.name = name;
state.signInOnly = false;
open();
stepWallet();
}
// Sign-in-only entry point for the nav wallet dropdown. mode = 'new' opens
// the "create wallet" step directly, 'import' opens "import phrase", 'wc'
// starts a WizardConnect session. On success the flow writes siriusProfile
// and closes — no name context, no Fund/Confirm.
function startSignIn(mode) {
state.name = null;
state.signInOnly = true;
open();
if (mode === "new") stepCreate();
else if (mode === "import") stepImport();
else if (mode === "wc") stepExternal();
else stepWallet();
}
// Public API
window.siriusRegisterName = startFlow;
window.siriusSignInWallet = startSignIn;
// ---------- step 1: wallet choice ----------
function stepWallet() {
const saved = BNS.BuiltInWallet.exists();
render(`
<h3>Register ${esc(state.name)}</h3>
<p class="sub">First, the wallet that will <b>own</b> the name. The certificate is minted straight
to it — we never hold it.</p>
${saved ? `<button class="wopt" id="w-unlock"><span class="ic">🔓</span><span>
<b>Unlock my browser wallet</b>
<span>You already created one on this device.</span></span></button>` : ``}
<button class="wopt" id="w-new"><span class="ic">✨</span><span>
<b>Create a new wallet<span class="pill">easiest</span></b>
<span>Made here in your browser. You get a recovery phrase to write down — it is the only key.</span></span></button>
<button class="wopt" id="w-import"><span class="ic">🔑</span><span>
<b>Import a recovery phrase</b>
<span>Restore a wallet you already have, from its 12-word phrase.</span></span></button>
<button class="wopt" id="w-wiz"><span class="ic">🪄</span><span>
<b>Connect a wallet — WizardConnect<span class="pill">most private</span></b>
<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
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>
`);
if (saved) $("w-unlock").onclick = stepUnlock;
$("w-new").onclick = stepCreate;
$("w-import").onclick = stepImport;
$("w-wiz").onclick = () => stepExternal("wizard");
}
// ---------- WizardConnect (external wallet) ----------
async function stepExternal() {
const label = "WizardConnect";
render(`<h3>Connect with ${esc(label)}</h3><p class="sub">Starting the connection…</p>`);
let session;
try {
const mod = await import("https://silentmode.st/js/wizardconnect.js");
session = await mod.openWizardSession({
dappName: BNS.REGISTRAR.wizardConnect.dappName,
dappIcon: BNS.REGISTRAR.wizardConnect.dappIcon,
prefix: BNS.REGISTRAR.addressPrefix,
crypto: {
pubkeyToAddress: BNS.pubkeyToAddress,
pubkeyToLockingBytecode: BNS.pubkeyToLockingBytecode,
binToHex: BNS.binToHex,
txidOfHex: BNS.txidOfHex,
},
});
} catch (e) {
render(`<h3>Could not start ${esc(label)}</h3><p class="sub">${esc(e.message)}</p>
<div class="row end"><button class="btn ghost" id="back">Back</button></div>`);
$("back").onclick = state.signInOnly ? close : stepWallet;
return;
}
const uri = session.qrUri ?? session.uri;
render(`
<h3>Open your wallet</h3>
<p class="sub">Scan or paste this into ${esc(label)}-capable wallet, then approve the connection.</p>
<div class="addr"><code id="uri">${esc(uri)}</code><button class="btn ghost" id="copy">Copy</button></div>
<div class="note warn" style="margin-top:16px">Waiting for your wallet…
The link stays valid until you close this page.</div>
<div class="row end"><button class="btn ghost" id="cancel">Cancel</button></div>
`);
$("copy").onclick = () => navigator.clipboard?.writeText(uri);
$("cancel").onclick = async () => { try { await session.disconnect(); } catch {} (state.signInOnly ? close : stepWallet)(); };
try { await session.ready; } catch (e) { err(e); return; }
// Sign-in-only path: get the address and persist siriusProfile immediately,
// skip the whole "quote registration" branch since there is no name.
if (state.signInOnly) {
try {
const addresses = await session.getAddresses();
if (!addresses?.length) throw new Error("wallet returned no addresses");
state.session = session; // keep alive for record edits etc.
finishSignIn({
source: "wc",
address: addresses[0],
tokenAddress: BNS.toTokenAddress(addresses[0]),
watchedAddresses: addresses,
});
} catch (e) { err(e); }
return;
}
stepExternalConfirm(session, label);
}
async function stepExternalConfirm(session, label) {
render(`<h3>Connected</h3><p class="sub">Reading your wallet and building the registration…</p>`);
try {
const c = await client();
const addresses = await session.getAddresses();
const owner = addresses[0];
const balance = await BNS.getBalance(c, addresses);
const genesis = BNS.describeGenesisRequirement(balance.hasGenesisInput);
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>
<div class="row end"><button class="btn ghost" id="back">Back</button></div>`);
$("back").onclick = async () => { try { await session.disconnect(); } catch {} stepWallet(); };
return;
}
const quote = await BNS.quoteRegistration(c, { name: state.name, ownerAddress: owner, records: {} });
const rows = BNS.priceSummary(quote.costs).map((l) => `
<tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td>
<td>${sats(l.sats)} sat</td></tr>`).join("");
render(`
<h3>Confirm ${esc(quote.displayName)}</h3>
<p class="sub">Connected to ${esc(label)}. The certificate is minted to
<span class="mono">${esc(owner)}</span> — your wallet, your key.</p>
<table class="price">${rows}</table>
${genesis.ready ? "" : `<div class="note warn" style="margin-top:16px">
<b>Two approvals needed.</b> ${esc(genesis.explanation)}</div>`}
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="go">Register — approve in your wallet</button></div>
`);
$("back").onclick = async () => { try { await session.disconnect(); } catch {} stepWallet(); };
$("go").onclick = async () => {
render(`<h3>Approve in your wallet</h3>
<p class="sub">${esc(label)} has sent the transaction to your device. Check the details there
and approve. Do not close this tab.</p><div class="steps-log" id="log"></div>`);
const log = $("log");
const say = (s) => { const d = document.createElement("div"); d.textContent = s; log.appendChild(d); };
try {
const res = await BNS.registerWithExternalWallet(await client(), {
session, name: state.name, ownerAddress: owner, records: {}, onProgress: say,
});
state.result = res;
state.session = session;
stepDone();
} catch (e) {
err(e);
const row = document.createElement("div"); row.className = "row end";
row.innerHTML = `<button class="btn ghost" id="back">Back</button>`;
sheet.appendChild(row);
$("back").onclick = () => stepExternalConfirm(session, label);
}
};
} catch (e) { err(e); }
}
// ---------- built-in: create ----------
function stepCreate() {
// The Register button flow lands here via stepWallet, so "Back" returns to
// the wallet-choice step. The sign-in dropdown calls stepCreate directly
// with no chosen name, so "Back" should just close the modal in that case.
const goBack = state.signInOnly ? close : stepWallet;
render(`
<h3>Create your wallet</h3>
<p class="sub">Generated in this browser. The phrase is never sent anywhere — not to us, not to anyone.</p>
<label for="pw">Password (encrypts the wallet on this device)</label>
<input type="password" id="pw" autocomplete="new-password" placeholder="at least 8 characters">
<label for="pw2">Repeat password</label>
<input type="password" id="pw2" autocomplete="new-password">
<div class="note warn" style="margin-top:16px"><b>There is no reset.</b> The password protects this
device; the recovery phrase you will see next <i>is</i> the wallet. Lose both and the name is gone
forever — that is what self-custody means.</div>
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="next">Generate phrase →</button></div>
`);
$("back").onclick = goBack;
$("next").onclick = async () => {
const pw = $("pw").value, pw2 = $("pw2").value;
if (pw.length < 8) return err(new Error("password must be at least 8 characters"));
if (pw !== pw2) return err(new Error("passwords do not match"));
try {
const wallet = await BNS.BuiltInWallet.create();
await wallet.save(pw);
state.wallet = wallet;
stepPhrase();
} catch (e) { err(e); }
};
}
function stepPhrase() {
const words = state.wallet.mnemonic.split(" ");
render(`
<h3>Write this down. Now.</h3>
<p class="sub">These 12 words are your wallet. Anyone who has them owns your names; if you lose them,
nobody — including us — can recover anything.</p>
<div class="phrase">${words.map((w,i)=>`<span><b>${i+1}</b>${esc(w)}</span>`).join("")}</div>
<div class="row"><button class="btn ghost" id="copy">Copy phrase</button></div>
<label class="chk"><input type="checkbox" id="ack">
<span>I have written the phrase down somewhere safe and offline. I understand it cannot be reset.</span></label>
<div class="row end"><button class="btn acid" id="next" disabled>Continue →</button></div>
`);
$("copy").onclick = () => navigator.clipboard?.writeText(state.wallet.mnemonic);
$("ack").onchange = (e) => { $("next").disabled = !e.target.checked; };
$("next").onclick = () => { if (!finishSignIn(state.wallet)) stepFund(); };
}
// ---------- built-in: import / unlock ----------
function stepImport() {
const goBack = state.signInOnly ? close : stepWallet;
render(`
<h3>Import a recovery phrase</h3>
<p class="sub">12 words, separated by spaces. It stays in this browser.</p>
<label for="mn">Recovery phrase</label>
<textarea id="mn" spellcheck="false" placeholder="word word word …"></textarea>
<label for="pw">Password to encrypt it on this device</label>
<input type="password" id="pw" autocomplete="new-password">
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="next">Import →</button></div>
`);
$("back").onclick = goBack;
$("next").onclick = async () => {
try {
if ($("pw").value.length < 8) throw new Error("password must be at least 8 characters");
const wallet = await BNS.BuiltInWallet.fromMnemonic($("mn").value);
await wallet.save($("pw").value);
state.wallet = wallet;
if (!finishSignIn(wallet)) stepFund();
} catch (e) { err(e); }
};
}
function stepUnlock() {
const goBack = state.signInOnly ? close : stepWallet;
render(`
<h3>Unlock your wallet</h3>
<p class="sub">Decrypts the wallet stored in this browser.</p>
<label for="pw">Password</label>
<input type="password" id="pw" autocomplete="current-password">
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="next">Unlock →</button></div>
`);
$("back").onclick = goBack;
$("pw").onkeydown = (e) => { if (e.key === "Enter") $("next").click(); };
$("next").onclick = async () => {
try {
state.wallet = await BNS.BuiltInWallet.load($("pw").value);
if (!finishSignIn(state.wallet)) stepFund();
} catch (e) { err(e); }
};
}
// ---------- fund ----------
let fundTimer = null;
async function stepFund() {
const w = state.wallet;
const faucets = BNS.REGISTRAR.faucets
.map((f) => `<a href="${esc(f.url)}" target="_blank" rel="noopener" style="color:var(--acid,#d6ff3d)">${esc(f.name)}</a>`).join(" · ");
render(`
<h3>Fund your wallet</h3>
<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>
<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>
`);
$("copy").onclick = () => navigator.clipboard?.writeText(w.address);
$("back").onclick = () => { clearInterval(fundTimer); stepWallet(); };
$("next").onclick = () => { clearInterval(fundTimer); stepConfirm(); };
const poll = async () => {
try {
const b = await BNS.getBalance(await client(), w.watchedAddresses);
const enough = b.sats >= 20000n;
$("bal").innerHTML = b.sats > 0n
? `<b class="ok">${sats(b.sats)} sat</b> available (${b.coins} coin${b.coins===1?"":"s"})`
+ (enough ? "" : ` — a little more is needed to cover the registration.`)
: `Waiting for coins… this page is watching the chain live.`;
$("next").disabled = !enough;
} catch (e) { $("bal").textContent = "could not reach the chain: " + e.message; }
};
await poll();
clearInterval(fundTimer);
fundTimer = setInterval(poll, 6000);
}
// ---------- confirm ----------
async function stepConfirm() {
render(`<h3>Preparing…</h3><p class="sub">Building your registration transaction.</p>`);
try {
const c = await client();
const avail = await BNS.checkAvailability(c, state.name);
if (!avail.available) throw new Error(`"${avail.name}" was just registered by someone else`);
const quote = await BNS.quoteRegistration(c, {
name: state.name, ownerAddress: state.wallet.address, records: {},
});
state.quote = quote;
const rows = BNS.priceSummary(quote.costs).map((l) => `
<tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td>
<td>${sats(l.sats)} sat</td></tr>`).join("");
render(`
<h3>Confirm ${esc(quote.displayName)}</h3>
<p class="sub">One transaction. It mints the certificate to <b>your</b> address and pays the fees.</p>
<table class="price">${rows}</table>
<div class="note" style="margin-top:16px">The certificate will be minted to
<span class="mono">${esc(state.wallet.address)}</span> — your key, your name.
You can set where it points immediately afterwards.</div>
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="go">Register — sign &amp; broadcast</button></div>
`);
$("back").onclick = stepFund;
$("go").onclick = stepRegister;
} catch (e) {
render(`<h3>Could not prepare</h3><p class="sub">${esc(e.message)}</p>
<div class="row end"><button class="btn ghost" id="back">Back</button></div>`);
$("back").onclick = stepFund;
}
}
// ---------- register ----------
async function stepRegister() {
render(`<h3>Registering ${esc(state.name)}</h3>
<p class="sub">Signing in your browser and broadcasting to the chain. Do not close this tab.</p>
<div class="steps-log" id="log"></div>`);
const log = $("log");
const say = (s) => { const d = document.createElement("div"); d.textContent = s; log.appendChild(d); log.scrollTop = log.scrollHeight; };
const words = {
"checking-availability": "checking the name is still free",
"loading-coins": "loading your coins",
"preparing-wallet": "preparing your wallet (one-off setup transaction)",
"prepared": "wallet prepared",
"building": "building the registration transaction",
"signing": "signing with your key — in this browser",
"broadcasting": "broadcasting to the Bitcoin Cash network",
"registered": "registered",
};
try {
const res = await BNS.registerWithBuiltInWallet(await client(), {
wallet: state.wallet, name: state.name, records: {},
onProgress: (s) => say(words[s] ?? s),
});
state.result = res;
stepDone();
} catch (e) {
err(e);
const row = document.createElement("div"); row.className = "row end";
row.innerHTML = `<button class="btn ghost" id="back">Back</button>`;
sheet.appendChild(row);
$("back").onclick = stepConfirm;
}
}
// ---------- done + point somewhere ----------
function stepDone() {
const r = state.result;
render(`
<h3><span class="ok">✓</span> ${esc(r.displayName)} is yours</h3>
<p class="sub">The certificate is on the chain, held by your key. Nobody can take it back.</p>
<table class="price">
<tr><td>Transaction</td><td>${esc(r.txid.slice(0,20))}…</td></tr>
<tr><td>Certificate ID</td><td>${esc(r.category.slice(0,20))}…</td></tr>
<tr><td>Owner</td><td>${esc(r.ownerAddress.slice(0,22))}…</td></tr>
</table>
<h3 style="margin-top:24px;font-size:1.05rem">Point it somewhere</h3>
<p class="sub">Optional, and changeable any time — only your key can.</p>
<label for="kind">Record type</label>
<select id="kind">
<option value="h">A tiny site stored on the chain itself (h)</option>
<option value="u">Redirect to a web address (u)</option>
<option value="ip">Your own server's IPv4 address (ip)</option>
<option value="tls">TLS certificate fingerprint (tls)</option>
</select>
<label for="val" id="vl">HTML — kept small; it lives inside the transaction</label>
<textarea id="val" spellcheck="false" placeholder="&lt;h1&gt;hello&lt;/h1&gt;"></textarea>
<div class="muted" id="budget"></div>
<div class="row end"><button class="btn ghost" id="skip">Done for now</button>
<button class="btn acid" id="set">Publish record</button></div>
`);
const labels = {
h: ["HTML — kept small; it lives inside the transaction", "<h1>hello</h1>"],
u: ["The https:// address the name should open", "https://example.org"],
ip: ["IPv4 address of your server", "203.0.113.9"],
tls: ["SHA-256 fingerprint of your TLS certificate (hex)", "a1b2c3…"],
};
const budget = () => {
const used = new TextEncoder().encode($("val").value).length;
const max = 200 - 30 - r.name.length;
$("budget").textContent = `${used} / ~${max} bytes used`;
$("budget").style.color = used > max ? "var(--taken,#f6768a)" : "";
};
$("kind").onchange = () => { const [l, p] = labels[$("kind").value]; $("vl").textContent = l; $("val").placeholder = p; budget(); };
$("val").oninput = budget;
budget();
$("skip").onclick = close;
$("set").onclick = async () => {
const records = { [$("kind").value]: $("val").value };
const external = !state.wallet && state.session;
render(`<h3>Publishing record</h3><p class="sub">This moves the certificate — which is how the chain
proves you are the owner.${external ? " Approve it in your wallet." : ""}</p>
<div class="steps-log" id="log"></div>`);
const log = $("log");
const say = (s) => { const d = document.createElement("div"); d.textContent = s; log.appendChild(d); };
try {
const upd = external
? await BNS.setRecordsWithExternalWallet(await client(), {
session: state.session, name: r.displayName, records, onProgress: say,
})
: await BNS.setRecordsWithBuiltInWallet(await client(), {
wallet: state.wallet, name: r.displayName, records, onProgress: say,
});
render(`<h3><span class="ok">✓</span> ${esc(r.displayName)} now points somewhere</h3>
<p class="sub">Update transaction <span class="mono">${esc(upd.txid.slice(0,20))}…</span> is on the chain.
Resolvers pick it up within a block.</p>
<div class="note">Open it with the Silent Mode resolver, or preview it at
<a href="https://navigate.st/bns/${encodeURIComponent(r.displayName)}" target="_blank" rel="noopener" style="color:var(--acid,#d6ff3d)">navigate.st</a>.</div>
<div class="row end"><button class="btn acid" id="fin">Finish</button></div>`);
$("fin").onclick = close;
} catch (e) {
err(e);
const row = document.createElement("div"); row.className = "row end";
row.innerHTML = `<button class="btn ghost" id="back">Back</button>`;
sheet.appendChild(row); $("back").onclick = stepDone;
}
};
}