Two gaps the owner panel left open. First, a hidden TLD was only a UI gate: anyone could still broadcast a REG under it and every indexer admitted it. Second, there was no way to sell a name without trusting the other side. Co-sign rule (consensus, applied in lockstep by bns.js and resolver-web.js): a REG under a TLD whose records at that height say policy "cosign" or hidden 1 is indexed only if the transaction carries the TLD's own certificate. The certificate can only be spent by the owner's key and is re-issued to them in the same transaction, so it is a co-signature nobody can forge and nothing is consumed. The TLD map now keeps the TUPD timeline so policy is evaluated at the REG height. Owners register under their private TLDs with the certificate added from their own wallet; third parties under a "cosign" TLD build the full transaction, sign their inputs and queue it at /api/cosign, where the owner approves it from the dashboard (signCosignRequest refuses to sign unless the certificate returns to the same locking script). Marketplace: a listing is the seller's certificate input plus a price output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a bulletin board (/api/market, verified against the on-chain owner and pruned when the certificate moves). The buyer completes it in one transaction, so the seller is paid exactly when the name moves. Cancelling also spends the certificate once so the offer is void. Site: market.html, Sell sub-tab and Pending approvals in the portal, Market link in nav and footer, six dictionaries extended, cache tags bumped. Verified on chipnet: cosigned.sc registered by a throwaway wallet through the queue with the .sc certificate back at the owner; aloevera.test listed and delisted through the API. Ariadne's resolver-web.js copy and the mobile Bns.java port still need the co-sign rule; until then they admit REGs this index rejects.
1333 lines
69 KiB
JavaScript
1333 lines
69 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=20260917market";
|
||
|
||
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 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" });
|
||
// Persistent device session (js/session.js): remember the phrase under a
|
||
// non-extractable browser key so the next visit signs in silently. Only
|
||
// for the built-in wallet — external (WizardConnect) sessions cannot be
|
||
// resumed without the other wallet.
|
||
if (w?.mnemonic && window.siriusSession?.remember) {
|
||
window.siriusSession.remember(w.mnemonic, { accountPath: w.accountPath }).catch(() => {});
|
||
}
|
||
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;
|
||
}
|
||
|
||
// Return the live wallet if one is already in memory; otherwise try the
|
||
// persistent device session (js/session.js) — the phrase is decrypted with
|
||
// a browser-held key, never stored in plaintext. Falls back to false, which
|
||
// sends the flow to the wallet-choice / Unlock screen.
|
||
let restoring = null;
|
||
async function restoreSessionWallet() {
|
||
if (window.siriusWallet) return window.siriusWallet;
|
||
if (!window.siriusSession?.restore) return null;
|
||
if (!restoring) {
|
||
restoring = (async () => {
|
||
try {
|
||
const s = await window.siriusSession.restore();
|
||
if (!s?.mnemonic) return null;
|
||
const w = s.accountPath
|
||
? await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, undefined, s.accountPath)
|
||
: await BNS.BuiltInWallet.fromMnemonic(s.mnemonic);
|
||
if (!window.siriusWallet) {
|
||
window.siriusWallet = w;
|
||
// Keep the profile breadcrumb in step (address may have been
|
||
// cleared by a sign-out in another tab that did not reach here).
|
||
writeProfile({ address: w.address, tokenAddress: w.tokenAddress, source: "seed" });
|
||
}
|
||
return window.siriusWallet;
|
||
} catch { return null; }
|
||
finally { restoring = null; }
|
||
})();
|
||
}
|
||
return restoring;
|
||
}
|
||
window.siriusRestoreSession = restoreSessionWallet;
|
||
async function adoptSessionWalletAsync() {
|
||
if (adoptSignedInWallet()) return true;
|
||
const w = await restoreSessionWallet();
|
||
if (w) return adoptSignedInWallet();
|
||
return false;
|
||
}
|
||
|
||
async function startFlow(name, opts = {}) {
|
||
state.name = name;
|
||
state.tld = null;
|
||
state.signInOnly = false;
|
||
// Service fee: the TLD owner's flat price when they set one, else the
|
||
// length tier (siriusPricing owns both 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, tldRaw] = String(name).split(".");
|
||
const tld = tldRaw || "bch";
|
||
const sp = window.siriusPricing;
|
||
if (sp?.tldReady) await sp.tldReady();
|
||
// A TLD its owner switched off is private: the public cannot buy under
|
||
// it, but the owner still can (from the portal's TLDs tab). Frozen means
|
||
// nobody, owner included. Check against the signed-in wallet's addresses
|
||
// so the owner's own purchase goes through.
|
||
const mine = (window.siriusWallet?.watchedAddresses)
|
||
|| (window.siriusWallet?.address ? [window.siriusWallet.address] : [])
|
||
|| [];
|
||
const sell = sp?.tldSellable ? sp.tldSellable(tld, mine) : { ok: true, owner: false };
|
||
if (!sell.ok) {
|
||
open();
|
||
const why = sell.reason === "frozen"
|
||
? `The owner of <b>.${esc(tld)}</b> has frozen it — no new names are being registered under it right now.`
|
||
: `<b>.${esc(tld)}</b> is a private TLD — its owner has switched it off for the public. Only the owner can register names under it right now. Check back later or pick another TLD.`;
|
||
render(`<h3>${esc(name)} is not available</h3><p class="sub">${why}</p>
|
||
<div class="row end"><button class="btn ghost" id="back">Close</button></div>`);
|
||
$("back").onclick = close;
|
||
return;
|
||
}
|
||
const p = sp?.priceForName(label, tld);
|
||
state.ownerMint = !!sell.owner;
|
||
// Co-sign rule (consensus): a REG under a TLD whose policy is `cosign` or
|
||
// `hidden` is only admitted if the transaction also carries the TLD's own
|
||
// certificate. Owner: add it from this wallet. Anyone else under a cosign
|
||
// TLD: build the transaction, sign our inputs, and park it in the gateway
|
||
// queue for the owner to co-sign and broadcast.
|
||
const pol = sell.policy || {};
|
||
state.cosign = sell.owner && (pol.hidden || pol.cosign) && pol.category ? { tld, category: pol.category } : null;
|
||
state.cosignRequest = sell.reason === "cosign" && pol.category ? { tld, category: pol.category, owner: pol.owner } : null;
|
||
// Owner registering under their own TLD: they would receive 90% of the
|
||
// fee from themselves, so only the platform's share is charged. The
|
||
// registrar lib already routes the whole fee to the platform when the
|
||
// TLD owner is the buyer (no self-payment output).
|
||
const ownerRate = (sats) => (sats == null ? null : BigInt(sats) / 10n);
|
||
state.nameFeeSats = opts.serviceFeeSats != null
|
||
? BigInt(opts.serviceFeeSats)
|
||
: (p ? (sell.owner ? ownerRate(p.sats) : 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(); }
|
||
if (state.cosign || state.cosignRequest) {
|
||
render(`<h3>Use the built-in wallet for this TLD</h3>
|
||
<p class="sub">Names under <b>.${esc(state.cosignRequest?.tld || state.cosign?.tld)}</b> need the TLD owner's co-signature in the same transaction. That flow is built for the browser wallet; external-wallet support lands in the next drop.</p>
|
||
<div class="row end"><button class="btn ghost" id="back">Back</button></div>`);
|
||
$("back").onclick = async () => { try { await session.disconnect(); } catch {} stepWallet(); };
|
||
return;
|
||
}
|
||
|
||
const 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();
|
||
// A persistent device session signs in without any prompt.
|
||
if (!state.wallet && window.siriusSession?.hasSession?.()) {
|
||
render(`${renderTabs("unlock")}<h3>Signing you in…</h3><p class="sub">Restoring your session on this device.</p>`);
|
||
restoreSessionWallet().then((w) => {
|
||
if (w) { state.wallet = w; if (!finishSignIn(w)) stepFund(); }
|
||
else stepUnlock();
|
||
});
|
||
return;
|
||
}
|
||
// 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">4–6 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 4–6 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();
|
||
// With "stay signed in" on (the default) the device session already
|
||
// makes the next visit prompt-free; a PIN would just be one more thing
|
||
// to type. It stays available in the dashboard Settings for people who
|
||
// want a gate before payments.
|
||
if (window.siriusSession?.isEnabled?.()) return next();
|
||
render(`
|
||
${renderTabs("unlock")}
|
||
<h3>Set a PIN <span class="pill">optional</span></h3>
|
||
<p class="sub">4–6 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 4–6 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>
|
||
${state.ownerMint ? `<div class="note">You own this TLD, so this is an <b>owner registration</b>: only the platform's 10% share of the name price is charged — the 90% owner share would just be paid to yourself.</div>` : ""}
|
||
${state.cosign ? `<div class="note">Private TLD: your <b>.${esc(state.cosign.tld)}</b> certificate is spent and re-issued to you in the same transaction — that is the co-signature the registry requires.</div>` : ""}
|
||
${state.cosignRequest ? `<div class="note warn"><b>Needs the TLD owner's approval.</b> .${esc(state.cosignRequest.tld)} is a co-sign TLD. You sign your part now; the owner co-signs and broadcasts from their dashboard. Nothing leaves your wallet until they do, and if they decline the request simply expires.</div>` : ""}
|
||
<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">${state.cosignRequest ? "Request approval — sign" : "Register — sign & broadcast"}</button></div>
|
||
`);
|
||
$("back").onclick = stepFund;
|
||
$("go").onclick = () => stepApprove({ feeSats: state.nameFeeSats ?? 0n, kind: "name", back: stepConfirm, next: state.cosignRequest ? stepRequestCosign : 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 = () => 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>`;
|
||
|
||
// Signed in with a live wallet and no "ask for PIN before payments"
|
||
// preference: approving is one click. The signature itself is the proof;
|
||
// re-typing a secret the browser already holds adds nothing but friction.
|
||
const gateOff = !(window.siriusSession?.requirePinForPayments?.());
|
||
if (state.wallet && gateOff) {
|
||
render(`
|
||
<h3>Approve payment</h3>
|
||
<p class="sub">Sign and broadcast the registration for ${target}. Fee: ${feeLine}.
|
||
You can require a PIN for every payment under Settings in the dashboard.</p>
|
||
<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").onclick = next;
|
||
$("approve").focus();
|
||
return;
|
||
}
|
||
|
||
// 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 4–6 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.cosign ? { cosign: state.cosign } : {}),
|
||
});
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ---------- co-sign request (third party under a cosign TLD) ----------
|
||
// Build the full registration with the TLD owner's certificate as the last
|
||
// input, sign only our own inputs, hand the half-signed transaction to the
|
||
// gateway queue. The owner signs the certificate input and broadcasts.
|
||
const GW = "https://silentmode.st";
|
||
async function stepRequestCosign() {
|
||
const cr = state.cosignRequest;
|
||
render(`<h3>Requesting approval for ${esc(state.name)}</h3>
|
||
<p class="sub">Building and signing your half of the transaction. 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 with the owner's certificate",
|
||
"signing": "signing your inputs — in this browser",
|
||
};
|
||
try {
|
||
say(`locating the .${cr.tld} certificate`);
|
||
const cres = await fetch(`${GW}/api/tld-cert/${encodeURIComponent(cr.tld)}`, { cache: "no-store" });
|
||
const cert = await cres.json();
|
||
if (!cres.ok) throw new Error(cert.error || `API ${cres.status}`);
|
||
const built = await BNS.buildCosignRequest(await client(), {
|
||
wallet: state.wallet, name: state.name, records: {}, tldCert: cert,
|
||
onProgress: (s) => say(words[s] ?? s),
|
||
...(state._feeOverride ?? {}),
|
||
});
|
||
say("submitting to the approval queue");
|
||
const r = await fetch(`${GW}/api/cosign`, { method: "POST", headers: { "content-type": "application/json" },
|
||
body: JSON.stringify({ name: built.name, buyer: state.wallet.address, tx: built.tx, sourceOutputs: built.sourceOutputs, note: "" }) });
|
||
const j = await r.json().catch(() => ({}));
|
||
if (!r.ok) throw new Error(j.error || `API ${r.status}`);
|
||
state.cosignId = j.id;
|
||
stepAwaitCosign();
|
||
} 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;
|
||
}
|
||
}
|
||
async function stepAwaitCosign() {
|
||
const cr = state.cosignRequest;
|
||
render(`
|
||
<h3>Waiting for the owner of .${esc(cr.tld)}</h3>
|
||
<p class="sub">Your request <span class="mono">${esc(state.cosignId)}</span> is in the queue. The TLD owner sees it in their dashboard and
|
||
co-signs and broadcasts it in one click. Your coins stay in your wallet until then — if you spend them elsewhere, the request simply becomes invalid.</p>
|
||
<div class="note">Requests expire after 7 days. You can close this window; when the owner approves, <b>${esc(state.name)}</b> appears in your dashboard.</div>
|
||
<div class="status" id="poll">Checking every 20 seconds…</div>
|
||
<div class="row end"><button class="btn danger" id="withdraw">Withdraw request</button><button class="btn ghost" id="done">Close</button></div>
|
||
`);
|
||
let stop = false;
|
||
$("done").onclick = () => { stop = true; close(); };
|
||
$("withdraw").onclick = async () => {
|
||
try {
|
||
const ts = String(Date.now());
|
||
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`BNS-COSIGN1\n${state.cosignId}\n${ts}`)));
|
||
const sig = btoa(String.fromCharCode(...state.wallet.signMessage(digest)));
|
||
await fetch(`${GW}/api/cosign/${encodeURIComponent(state.cosignId)}`, { method: "DELETE", headers: { "x-bns-ts": ts, "x-bns-sig": sig } });
|
||
} catch {}
|
||
stop = true; close();
|
||
};
|
||
const tick = async () => {
|
||
if (stop || !document.body.contains($("poll"))) return;
|
||
try {
|
||
const r = await fetch(`${GW}/api/cosign/${encodeURIComponent(state.cosignId)}`, { cache: "no-store" });
|
||
if (r.status === 404) {
|
||
const avail = await BNS.checkAvailability(await client(), state.name);
|
||
if (!avail.available) {
|
||
render(`<h3><span class="ok">✓</span> ${esc(state.name)} is yours</h3>
|
||
<p class="sub">The owner of .${esc(cr.tld)} co-signed and broadcast your registration. Set records and content from your dashboard.</p>
|
||
<div class="row end"><a class="btn acid" href="./portal.html#domains">Open dashboard →</a></div>`);
|
||
} else {
|
||
render(`<h3>Request declined</h3><p class="sub">The owner of .${esc(cr.tld)} removed your request. Nothing was spent.</p>
|
||
<div class="row end"><button class="btn ghost" id="done">Close</button></div>`);
|
||
$("done").onclick = close;
|
||
}
|
||
return;
|
||
}
|
||
} catch {}
|
||
setTimeout(tick, 20000);
|
||
};
|
||
setTimeout(tick, 20000);
|
||
}
|
||
|
||
// ---------- 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;
|
||
}
|
||
};
|
||
}
|