sirius/js/register-flow.js
Local Dev caf11c2512 feat(sirius-x): raise TLD pricing to $25–$500; PIN gate on payment approval
TLD price tiers (pricing.js):
  1 char  $500  (was $10)
  2 char  $250  (was $8)
  3 char  $150  (was $6)
  4-6ch   $100  (was $5)
  7-10ch  $50   (was $5)
  11+ch   $25   (was $4)

Payment approval (register-flow.js):
  New stepApprove(...) gates the actual sign+broadcast on an explicit
  user gesture — PIN pad when a PIN blob exists on this device, wallet
  password otherwise. Same "3 wrong PINs → wipe, fall back to password"
  behavior as the sign-in unlock step. Wired between stepConfirm(Tld)?
  and stepRegister(Tld)? so both name mints and TLD mints require
  approval even when the wallet is already in memory.

tld.html fee card: reflects the new $25–$500 range instead of the
"≈ 1,250,000 sat · ≈ $5" placeholder.
2026-09-09 03:45:41 +02:00

1128 lines
57 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=20260908split";
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 .signin-tabs{
display:flex;gap:2px;margin:-8px -12px 18px;padding:0 4px 0;
border-bottom:1px solid var(--line,rgba(255,255,255,.09));overflow-x:auto
}
#sirius-register-modal .signin-tabs button{
background:none;border:none;color:var(--mut,#8b98a9);font-family:inherit;font-size:12.5px;
padding:10px 12px;cursor:pointer;border-bottom:2px solid transparent;
white-space:nowrap;letter-spacing:.2px;transition:color .1s,border-color .1s
}
#sirius-register-modal .signin-tabs button:hover{color:var(--ink,#e7eaf1)}
#sirius-register-modal .signin-tabs button.active{
color:var(--acid,#d6ff3d);border-bottom-color:var(--acid,#d6ff3d)
}
/* Inline-sheet variant: same step content styling as the modal, but
rendered as a normal page section (no fixed positioning, no backdrop,
no shadow). Portal.html uses this so the tabbed sign-in IS the page. */
.sirius-inline-sheet{
background:var(--panel,#141a24);border:1px solid var(--line,rgba(255,255,255,.09));
border-radius:16px;padding:22px 24px;color:var(--ink,#e7eaf1)
}
.sirius-inline-sheet h3{margin:0 0 .2rem;font-size:1.15rem}
.sirius-inline-sheet .sub{color:var(--mut,#8b98a9);font-size:14px;margin:0 0 1.2rem}
.sirius-inline-sheet label{display:block;font-size:13px;color:var(--mut,#8b98a9);margin:14px 0 5px}
.sirius-inline-sheet input[type=text],
.sirius-inline-sheet input[type=password],
.sirius-inline-sheet textarea,
.sirius-inline-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-inline-sheet textarea{min-height:76px;resize:vertical;font-family:ui-monospace,monospace;font-size:13px}
.sirius-inline-sheet input:focus,.sirius-inline-sheet textarea:focus{border-color:#4b7bec}
.sirius-inline-sheet .row{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}
.sirius-inline-sheet .row.end{justify-content:flex-end}
.sirius-inline-sheet .phrase{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}
.sirius-inline-sheet .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-inline-sheet .phrase span b{color:var(--dim,#5e6678);font-weight:400;margin-right:6px;font-size:11px}
.sirius-inline-sheet .addr{display:flex;gap:8px;align-items:center;margin-top:8px}
.sirius-inline-sheet .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-inline-sheet .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-inline-sheet .steps-log div::before{content:" ";color:var(--acid,#d6ff3d)}
.sirius-inline-sheet .err{color:var(--taken,#f6768a);font-size:13.5px;margin-top:12px}
.sirius-inline-sheet .ok{color:var(--ok,#4fd1a5)}
.sirius-inline-sheet .note{background:var(--panel2,#18202c);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-inline-sheet .note.warn{border-left-color:var(--warn,#ffc75f)}
.sirius-inline-sheet .note b{color:var(--ink,#e7eaf1)}
.sirius-inline-sheet .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-inline-sheet .chk{display:flex;gap:9px;align-items:flex-start;margin-top:14px;font-size:13.5px;color:var(--mut,#8b98a9)}
.sirius-inline-sheet .chk input{margin-top:3px}
.sirius-inline-sheet .signin-tabs{
display:flex;gap:2px;margin:-6px -8px 18px;padding:0 4px;
border-bottom:1px solid var(--line,rgba(255,255,255,.09));overflow-x:auto
}
.sirius-inline-sheet .signin-tabs button{
background:none;border:none;color:var(--mut,#8b98a9);font-family:inherit;font-size:13px;
padding:10px 14px;cursor:pointer;border-bottom:2px solid transparent;
white-space:nowrap;letter-spacing:.2px;transition:color .1s,border-color .1s
}
.sirius-inline-sheet .signin-tabs button:hover{color:var(--ink,#e7eaf1)}
.sirius-inline-sheet .signin-tabs button.active{
color:var(--acid,#d6ff3d);border-bottom-color:var(--acid,#d6ff3d)
}
.sirius-inline-sheet .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-inline-sheet .btn.ghost{background:transparent;border:1px solid var(--line,rgba(255,255,255,.09));color:var(--ink,#e7eaf1)}
.sirius-inline-sheet .btn.acid{background:var(--acid,#d6ff3d);color:#0b0e14;font-weight:600}
.sirius-inline-sheet .btn:hover{filter:brightness(1.12)}
.sirius-inline-sheet .btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
.sirius-inline-sheet .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-inline-sheet .wopt:hover{border-color:#4b7bec}
.sirius-inline-sheet .wopt .ic{font-size:22px;line-height:1}
.sirius-inline-sheet .wopt b{display:block;font-size:14.5px}
.sirius-inline-sheet .wopt span{color:var(--mut,#8b98a9);font-size:13px}
#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.
// inlineTarget: when set, the flow renders into that page element instead
// of the modal sheet (portal.html uses this to embed the tabbed sign-in as
// the page's main content). The modal is left closed in inline mode.
const state = { name: null, signInOnly: false, inlineTarget: null, wallet: null, session: null, client: null, quote: null, result: null };
const modal = $("sirius-register-modal");
const modalSheet = $("sirius-reg-sheet");
// `sheet` is the element the step functions render into — the modal's sheet
// by default, or an inline container when siriusRenderSignInInline() is
// active. `let` so it can be reassigned per call.
let sheet = modalSheet;
function open() {
// Inline mode never opens the modal — the sign-in UI lives on the page.
if (state.inlineTarget) return;
modal.classList.add("open");
}
function close() {
// In inline mode we can't "close" — leave the content in place. State is
// still reset so the next call starts fresh.
if (!state.inlineTarget) 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;
// Restore the modal sheet as the default render target so subsequent
// register/mint flows (which are modal-based) work.
if (state.inlineTarget) {
state.inlineTarget = null;
sheet = modalSheet;
}
}
// 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) {
// Expose the live wallet in memory only, BEFORE writeProfile so any
// listener of siriusProfileChanged (portal.html adoptWalletFromModal)
// sees it and can switch to the signed-in view synchronously. The
// mnemonic is NEVER written to sessionStorage / localStorage in
// plaintext — the only at-rest copies are the password-encrypted
// BuiltInWallet blob and, optionally, the PIN-encrypted escrow blob.
window.siriusWallet = 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(); });
// Step functions render into `sheet`, which is reassigned per call by
// renderSignInInline() when the flow is mounted inline on a host page
// (portal.html). Modal by default. Direct write — no separate variable
// needed since `sheet` above is the single mutable target.
function render(html) { sheet.innerHTML = html; }
// Tab bar for sign-in mode — lets the user jump between the four wallet
// actions without going back to a wallet-choice screen. Prepended to the
// sheet content by each sign-in step. Only shown in signInOnly mode.
function renderTabs(active) {
if (!state.signInOnly) return "";
const saved = BNS.BuiltInWallet.exists();
// The four tabs are always present so the layout is stable across sessions.
// When there is no saved wallet on this device, the Unlock tab still shows
// — clicking it lands on a stepUnlock screen that explains why unlock is
// not available and offers a shortcut to Create or Import instead.
const tabs = [
["unlock", "🔓 Unlock", saved ? "Wallet stored on this device" : "No saved wallet on this device"],
["new", "🆕 Create", "Sign up · fresh phrase"],
["import", "📥 Import", "Sign in · seed you have"],
["wc", "🔗 WizardConnect", "Cashonize / Paytaca"],
];
return `
<div class="signin-tabs" role="tablist">
${tabs.map(([id, label, hint]) => `
<button role="tab" data-tab="${id}" class="${id === active ? "active" : ""}"
title="${esc(hint)}">${label}</button>
`).join("")}
</div>
`;
}
// Wire tab clicks — document-level delegation so both the modal sheet and
// any inline container hosting the tabs work with one listener. Switching
// away from a live WC session tears it down first so we don't leak
// connections. Guarded to only match .signin-tabs buttons we render.
document.addEventListener("click", async (e) => {
const t = e.target.closest(".signin-tabs [data-tab]");
if (!t) return;
const which = t.dataset.tab;
if (which !== "wc" && state.session) {
try { await state.session.disconnect(); } catch {}
state.session = null;
}
if (which === "unlock") stepUnlock();
else if (which === "new") stepCreate();
else if (which === "import") stepImport();
else if (which === "wc") stepExternal();
});
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;
}
// If the user already unlocked a wallet in this tab (via inline sign-in on
// portal.html or a prior mint), the register/TLD mint flow can skip the
// wallet-choice step entirely and go straight to Fund / Confirm. That's
// what stops the flow from asking for the password a second time on a
// TLD buy right after unlocking.
function adoptSignedInWallet() {
if (window.siriusWallet && !state.wallet) {
state.wallet = window.siriusWallet;
// WC-signed-in wallets carry a `session`; keep it plumbed for the mint.
if (window.siriusWallet.source === "wc" && window.siriusWallet.session) {
state.session = window.siriusWallet.session;
}
return true;
}
return false;
}
// In-memory only: return the live wallet if it was set earlier this
// session, else false. NO plaintext-mnemonic cache — a reload drops the
// user back to the Unlock tab (PIN pad if a PIN blob exists, password
// field otherwise), which decrypts locally without any secret ever
// touching disk in plaintext.
async function adoptSessionWalletAsync() {
return adoptSignedInWallet();
}
async function startFlow(name, opts = {}) {
state.name = name;
state.tld = null;
state.signInOnly = false;
// Tier-based service fee (siriusPricing owns the tiers so landing search
// badges, tld cards and this mint agree). Caller can override with
// opts.serviceFeeSats when a coupon or manual price is in play.
const label = String(name).split(".")[0];
const p = window.siriusPricing?.priceForName(label);
state.nameFeeSats = opts.serviceFeeSats != null
? BigInt(opts.serviceFeeSats)
: (p ? p.sats : null);
open();
if (await adoptSessionWalletAsync()) stepFund();
else stepWallet();
}
// Start a TLD mint. Same wallet-choice + fund flow as a name register, but
// the mint step calls registerTldWith(Built|External)Wallet instead of the
// name variant, and the "done" screen skips the record editor since a TLD
// certificate goes to the operator's token address as-is.
async function startTldFlow(label, opts = {}) {
state.name = `.${label}`; // used for headings only
state.tld = { label, serviceFeeSats: BigInt(opts.serviceFeeSats ?? 0n) };
state.signInOnly = false;
open();
if (await adoptSessionWalletAsync()) stepFund();
else 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 = "your wallet"; // header text — used by stepWallet
state.tld = null;
state.signInOnly = true;
open();
// "choose" (default) shows the wallet-choice screen with all four options;
// the other modes are still supported as direct-action entry points from
// deep-links like portal.html?mode=new — but the nav dropdown funnels to
// "choose" so the user always sees the options first.
if (mode === "new") stepCreate();
else if (mode === "import") stepImport();
else if (mode === "wc") stepExternal();
else if (mode === "unlock") stepUnlock();
else stepWallet();
}
// Sign-in rendered inline on the host page (not in a modal). Pass a target
// element and portal-style tabs + form get painted into it. Used by
// portal.html so the "sign up / sign in" experience IS the page rather than
// an overlay on top of a launcher card.
function renderSignInInline(container, mode = "import") {
if (!container) return;
state.name = "your wallet";
state.tld = null;
state.signInOnly = true;
state.inlineTarget = container;
sheet = container;
// Give inline container the same visual bones as the modal sheet so the
// step content styling ports 1:1. Kept as a class so host CSS can override.
container.classList.add("sirius-inline-sheet");
if (mode === "new") stepCreate();
else if (mode === "import") stepImport();
else if (mode === "wc") stepExternal();
else if (mode === "unlock") stepUnlock();
else stepWallet();
}
// Public API
window.siriusRegisterName = startFlow;
window.siriusRegisterTld = startTldFlow;
window.siriusSignInWallet = startSignIn;
window.siriusRenderSignInInline = renderSignInInline;
// ---------- step 1: wallet choice ----------
function stepWallet() {
const saved = BNS.BuiltInWallet.exists();
const kind = state.tld ? "TLD" : "name";
const heading = state.signInOnly
? `Sign in <span style="color:var(--acid,#d6ff3d)">or</span> create a wallet`
: `Register ${esc(state.name)}`;
const intro = state.signInOnly
? `Pick one. Your keys stay in this browser — nothing is transmitted for sign-in.`
: `First, the wallet that will <b>own</b> the ${esc(kind)}. The certificate is minted straight to it — we never hold it.`;
render(`
<h3>${heading}</h3>
<p class="sub">${intro}</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(`${renderTabs("wc")}<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(`
${renderTabs("wc")}
<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;
}
// TLD external path: keep the session, hand off to the TLD confirm/mint
// steps which know to call registerTldWithExternalWallet.
if (state.tld) { state.session = session; return stepConfirmTld(); }
const feeOverride = state.nameFeeSats != null
? { serviceFee: { ...(BNS.REGISTRAR?.serviceFee ?? {}), sats: state.nameFeeSats } }
: {};
const quote = await BNS.quoteRegistration(c, { name: state.name, ownerAddress: owner, records: {}, ...feeOverride });
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, ...feeOverride,
});
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(`
${renderTabs("new")}
<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 = () => stepSetPin(state.wallet, () => { if (!finishSignIn(state.wallet)) stepFund(); });
}
// ---------- built-in: import / unlock ----------
function stepImport() {
const goBack = state.signInOnly ? close : stepWallet;
render(`
${renderTabs("import")}
<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;
stepSetPin(wallet, () => { if (!finishSignIn(wallet)) stepFund(); });
} catch (e) { err(e); }
};
}
function stepUnlock() {
const goBack = state.signInOnly ? close : stepWallet;
const saved = BNS.BuiltInWallet.exists();
// No saved wallet on this device — show why unlock isn't possible and
// route the user to Create/Import via the tab bar (which stays visible).
if (!saved) {
render(`
${renderTabs("unlock")}
<h3>No wallet on this device</h3>
<p class="sub">Unlock only works after you have created or imported a wallet on this browser.
Either <b>Create</b> a fresh one (writes an encrypted copy here you can unlock next time)
or <b>Import</b> a recovery phrase you already have.</p>
<div class="row end">
<button class="btn ghost" id="go-import" type="button">📥 Import a wallet →</button>
<button class="btn acid" id="go-new" type="button">🆕 Create a wallet →</button>
</div>
`);
$("go-import").onclick = stepImport;
$("go-new").onclick = stepCreate;
return;
}
// PIN escrow — if the user set a PIN on a previous session, prefer that.
// 3 wrong PINs wipe the blob and this branch falls back to the password
// form on next render.
if (window.siriusPin?.hasPin?.()) {
const remaining = window.siriusPin.attemptsRemaining();
render(`
${renderTabs("unlock")}
<h3>Enter your PIN</h3>
<p class="sub">46 digits. Set on this device to skip typing your full password.
${remaining < 3 ? `<b class="err" style="color:var(--taken,#f6768a)">${esc(String(remaining))} attempt${remaining === 1 ? "" : "s"} left</b> before the PIN resets and you must use your password.` : ""}</p>
<label for="pin">PIN</label>
<input type="password" id="pin" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="off">
<div class="row end">
<button class="btn ghost" id="use-password" type="button">Use password instead</button>
<button class="btn acid" id="next" type="button">Unlock →</button>
</div>
`);
$("use-password").onclick = () => { window.siriusPin.clear(); stepUnlock(); };
$("pin").focus();
$("pin").onkeydown = (e) => { if (e.key === "Enter") $("next").click(); };
$("next").onclick = async () => {
try {
const mnemonic = await window.siriusPin.tryUnlock($("pin").value);
state.wallet = await BNS.BuiltInWallet.fromMnemonic(mnemonic);
if (!finishSignIn(state.wallet)) stepFund();
} catch (e) {
if (e.locked) { render(`
${renderTabs("unlock")}
<h3>PIN reset</h3>
<p class="sub">3 wrong attempts — the PIN has been cleared. Enter your password to unlock and set a new PIN if you like.</p>
<div class="row end"><button class="btn acid" id="go-pw">Continue with password →</button></div>
`); $("go-pw").onclick = stepUnlock; return; }
err(e);
}
};
return;
}
render(`
${renderTabs("unlock")}
<h3>Unlock your wallet</h3>
<p class="sub">Decrypts the wallet stored in this browser. After unlocking you can set a
PIN so future sessions ask for 46 digits instead of the full password.</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);
// Fresh password unlock — offer the PIN shortcut before finishing.
stepSetPin(state.wallet, () => { if (!finishSignIn(state.wallet)) stepFund(); });
} catch (e) { err(e); }
};
}
// After a successful password sign-in (Import / Create / Unlock), offer to
// stash the mnemonic under a PIN so next session opens with a numeric pad.
// User can decline ("Not now"); either way we call `next` afterwards.
function stepSetPin(w, next) {
if (!window.siriusPin || !w?.mnemonic) return next();
// Already have a PIN blob? Don't overwrite silently — skip.
if (window.siriusPin.hasPin()) return next();
render(`
${renderTabs("unlock")}
<h3>Set a PIN <span class="pill">optional</span></h3>
<p class="sub">46 digits. Encrypts your recovery phrase under this PIN so future sessions on
this device unlock with just the PIN. Three wrong PINs and the PIN is cleared — you'd fall
back to your full password.</p>
<label for="pin1">PIN</label>
<input type="password" id="pin1" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="off">
<label for="pin2" style="margin-top:12px">Repeat PIN</label>
<input type="password" id="pin2" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="off">
<div class="row end">
<button class="btn ghost" id="skip" type="button">Not now</button>
<button class="btn acid" id="save-pin" type="button">Save PIN →</button>
</div>
`);
$("skip").onclick = next;
$("pin2").onkeydown = (e) => { if (e.key === "Enter") $("save-pin").click(); };
$("save-pin").onclick = async () => {
const a = $("pin1").value, b = $("pin2").value;
if (!/^\d{4,6}$/.test(a)) return err(new Error("PIN must be 46 digits"));
if (a !== b) return err(new Error("PINs do not match"));
try { await window.siriusPin.savePinBlob(w.mnemonic, a); next(); }
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();
// TLD flow skips the name-availability check and the per-name quote —
// registerTldWith*Wallet builds the tx internally and checks the on-chain
// TLD registry itself. Just show a simple confirm screen with the fee.
if (state.tld) return stepConfirmTld();
const avail = await BNS.checkAvailability(c, state.name);
if (!avail.available) throw new Error(`"${avail.name}" was just registered by someone else`);
const feeOverride = state.nameFeeSats != null
? { serviceFee: { ...(BNS.REGISTRAR?.serviceFee ?? {}), sats: state.nameFeeSats } }
: {};
const quote = await BNS.quoteRegistration(c, {
name: state.name, ownerAddress: state.wallet.address, records: {}, ...feeOverride,
});
state.quote = quote;
state._feeOverride = feeOverride;
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 = () => stepApprove({ feeSats: state.nameFeeSats ?? 0n, kind: "name", back: stepConfirm, next: 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;
}
}
// ---------- TLD confirm ----------
async function stepConfirmTld() {
const feeSats = state.tld.serviceFeeSats;
const owner = state.wallet ? state.wallet.address : (state.session ? (await state.session.getAddresses())[0] : "your address");
render(`
<h3>Confirm <b>${esc(state.name)}</b></h3>
<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>
<td>${sats(feeSats)} sat</td></tr>
<tr><td>Beacon dust + chain fee <small>(covered by wallet)</small></td>
<td>~1,300 sat</td></tr>
</table>
<div class="note" style="margin-top:16px">Minted to <span class="mono">${esc(owner)}</span>.
That wallet becomes the operator for this TLD.</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 = () => stepApprove({ feeSats, kind: "tld", back: stepConfirmTld, next: stepRegisterTld });
}
// ---------- approve payment (PIN gate) ----------
// After the user has confirmed the fee and clicked "Register — sign & broadcast",
// gate the actual sign+broadcast on a fresh PIN entry (or password when no PIN
// is set on this device). The gate exists so a wallet already unlocked in
// memory still can't be spent without an explicit user gesture — same idea as
// a hardware wallet asking for its PIN on every transaction.
//
// opts: { feeSats, kind: "name"|"tld", back, next }
function stepApprove(opts) {
const { feeSats, kind, back, next } = opts;
const feeLine = feeSats && feeSats > 0n ? `<b class="ok">${sats(feeSats)} sat</b>` : `<b class="ok">Service fee waived on chipnet</b>`;
const target = kind === "tld" ? `.<b>${esc(state.name)}</b>` : `<b>${esc(state.name)}</b>`;
// No PIN configured — prompt for the wallet password as the approval
// gesture. (When no wallet-at-rest exists either, `state.wallet` was set
// from a fresh Create/Import, so we ask the user to set a PIN afterwards.)
const usePinPad = !!window.siriusPin?.hasPin?.();
if (usePinPad) {
const remaining = window.siriusPin.attemptsRemaining();
render(`
<h3>Approve payment</h3>
<p class="sub">Enter your PIN to sign and broadcast the registration for ${target}. Fee: ${feeLine}.
${remaining < 3 ? `<b class="err" style="color:var(--taken,#f6768a)">${esc(String(remaining))} attempt${remaining === 1 ? "" : "s"} left</b> before the PIN resets and you must use your password.` : ""}</p>
<label for="approve-pin">PIN</label>
<input type="password" id="approve-pin" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="off">
<div class="row end">
<button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="approve">Approve →</button>
</div>
`);
$("back").onclick = back;
$("approve-pin").focus();
$("approve-pin").onkeydown = (e) => { if (e.key === "Enter") $("approve").click(); };
$("approve").onclick = async () => {
try {
const mnemonic = await window.siriusPin.tryUnlock($("approve-pin").value);
// PIN verified — ensure `state.wallet` is loaded from the mnemonic
// (unlock counter has been reset by tryUnlock).
if (!state.wallet) state.wallet = await BNS.BuiltInWallet.fromMnemonic(mnemonic);
next();
} catch (e) {
if (e.locked) {
render(`
<h3>PIN reset</h3>
<p class="sub">3 wrong attempts — the PIN has been cleared. Enter your password to
approve the payment and set a new PIN if you like.</p>
<div class="row end">
<button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="go-pw">Continue with password →</button>
</div>
`);
$("back").onclick = back;
$("go-pw").onclick = () => stepApprove(opts);
return;
}
err(e);
}
};
return;
}
// Fallback — no PIN blob. If we have a wallet-at-rest we ask for the
// password. If not (fresh Create/Import in this session), the wallet is
// already in memory and there is nothing to gate on locally, so proceed
// straight to sign + offer to set a PIN afterwards.
const hasWalletAtRest = !!(window.localStorage && localStorage.getItem("bns.wallet.v1"));
if (!hasWalletAtRest && state.wallet) { next(); return; }
render(`
<h3>Approve payment</h3>
<p class="sub">Enter your wallet password to sign and broadcast the registration for
${target}. Fee: ${feeLine}. Set a PIN afterwards so future payments approve with 46 digits.</p>
<label for="approve-pw">Password</label>
<input type="password" id="approve-pw" autocomplete="current-password">
<div class="row end">
<button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="approve">Approve →</button>
</div>
`);
$("back").onclick = back;
$("approve-pw").focus();
$("approve-pw").onkeydown = (e) => { if (e.key === "Enter") $("approve").click(); };
$("approve").onclick = async () => {
try {
state.wallet = await BNS.BuiltInWallet.load($("approve-pw").value);
// Offer to set a PIN so the next payment prompts for digits.
stepSetPin(state.wallet, next);
} catch (e) { err(e); }
};
}
async function stepRegisterTld() {
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 serviceFee = { address: BNS.REGISTRAR?.serviceFee?.address ?? null, sats: state.tld.serviceFeeSats };
try {
const res = state.session
? await BNS.registerTldWithExternalWallet(await client(), {
session: state.session, tld: state.tld.label, records: {},
serviceFee, tldListUrl: "https://navigate.st/api/tlds", onProgress: say,
})
: await BNS.registerTldWithBuiltInWallet(await client(), {
wallet: state.wallet, tld: state.tld.label, records: {},
serviceFee, tldListUrl: "https://navigate.st/api/tlds", onProgress: say,
});
state.result = res;
stepDoneTld();
} 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 = stepConfirmTld;
}
}
function stepDoneTld() {
const r = state.result;
render(`
<h3><span class="ok">✓</span> <b>.${esc(r.tld)}</b> is yours</h3>
<p class="sub">The TLD certificate is on the chain, held by your key. Second-level names under
<b>.${esc(r.tld)}</b> now live in your registry.</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>Operator</td><td>${esc(r.ownerAddress.slice(0,22))}…</td></tr>
</table>
<div class="row end"><button class="btn acid" id="fin">Done</button></div>
`);
$("fin").onclick = close;
}
// ---------- 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._feeOverride ?? {}),
});
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;
}
};
}