Two issues reported after an Import sign-in: 1. Done button was stuck — user saw the ✓ Signed in screen but the portal never switched to the names view. Root cause: finishSignIn fired siriusProfileChanged BEFORE setting window.siriusWallet, so the portal's listener called adoptWalletFromModal() while window.siriusWallet was still null, saw no wallet, and did nothing. Fix: expose the live wallet BEFORE writeProfile so the sync listener sees it and can enterPortal() immediately. 2. Every reload asked for the password again. Cache the wallet's mnemonic in sessionStorage on sign-in — same tab (or a page reload) rebuilds the BuiltInWallet silently via BuiltInWallet.fromMnemonic; a full browser close clears sessionStorage and the user is back at the Unlock tab. sessionStorage is per-origin per-tab so an XSS on Sirius.X pages would still be able to read it — that's the tradeoff for the convenience. Chipnet only; mainnet gets the PIN escrow pattern (3 wrong PIN tries → escalate to password) that Digibyte.x/web already uses. PIN implementation is deferred to its own commit. portal.html adoptWalletFromModal now tries sessionStorage after the in-memory check; register-flow.js startFlow/startTldFlow do the same via a new async adoptSessionWalletAsync so name and TLD mints on any page reuse the session wallet with no re-prompt. profile-menu.js sign-out clears sessionStorage + window.siriusWallet so signing out really does drop the user.
977 lines
50 KiB
JavaScript
977 lines
50 KiB
JavaScript
// 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) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[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 globally BEFORE writeProfile so any listener of
|
||
// siriusProfileChanged (portal.html adoptWalletFromModal) sees the wallet
|
||
// and can switch to the signed-in view synchronously.
|
||
window.siriusWallet = w;
|
||
// Session-cache the seed so a page reload restores the wallet without
|
||
// asking for the password again. sessionStorage is per-tab and clears on
|
||
// browser close — same lifetime as a "keep me signed in for this session"
|
||
// toggle in a normal app. Chipnet-only convenience; mainnet gets the PIN
|
||
// escrow pattern (see finishSignIn comment history).
|
||
try {
|
||
if (w?.mnemonic) sessionStorage.setItem("siriusSessionMnemonic", w.mnemonic);
|
||
if (w?.source === "wc") sessionStorage.setItem("siriusSessionSource", "wc");
|
||
} catch {}
|
||
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;
|
||
}
|
||
|
||
// Best-effort silent rebuild from sessionStorage — same session ⇒ same
|
||
// wallet, no password prompt. Returns true when the wallet is ready.
|
||
async function adoptSessionWalletAsync() {
|
||
if (state.wallet) return true;
|
||
if (adoptSignedInWallet()) return true;
|
||
try {
|
||
const cached = typeof sessionStorage !== "undefined"
|
||
? sessionStorage.getItem("siriusSessionMnemonic")
|
||
: null;
|
||
if (!cached) return false;
|
||
const w = await BNS.BuiltInWallet.fromMnemonic(cached);
|
||
window.siriusWallet = w;
|
||
state.wallet = w;
|
||
return true;
|
||
} catch { return false; }
|
||
}
|
||
|
||
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 = () => { 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;
|
||
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;
|
||
}
|
||
render(`
|
||
${renderTabs("unlock")}
|
||
<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();
|
||
// 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 & 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;
|
||
}
|
||
}
|
||
|
||
// ---------- 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 & broadcast</button></div>
|
||
`);
|
||
$("back").onclick = stepFund;
|
||
$("go").onclick = stepRegisterTld;
|
||
}
|
||
|
||
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="<h1>hello</h1>"></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;
|
||
}
|
||
};
|
||
}
|