Wallet dropdown items (New / Import / WizardConnect) were navigating
to portal.html?mode=X and asking the user to click again on arrival.
That is 'the sign in landing page which is not functioning' from the
user's perspective — a whole redirect for one form.
Now every dropdown item opens the mint modal (register-flow.js) inline
at the matching step, on whatever page the user is on:
New -> stepCreate ('Create your wallet' — password + generate)
Import -> stepImport ('Import a recovery phrase' — textarea)
WizardConnect -> stepExternal ('Open your wallet' — QR/URI)
register-flow.js grew a signInOnly mode: state.name is null, the step
Back buttons close instead of going to a wallet-choice step there is
no context for, and on wallet-loaded the flow writes siriusProfile
and shows a 'Signed in' confirmation instead of Fund -> Confirm -> Mint.
WC signInOnly keeps the session alive (state.session) so a later
record edit can reuse it without a fresh QR handshake.
profile-menu.js: menu items became <a data-action='new|import|wc'>
and the click handler lazy-loads register-flow.js on demand — the
docs/brand/theseus pages don't ship it in their initial payload, so
their nav pill loads it the first time a wallet button is clicked and
caches it for subsequent opens. Falls back to portal.html?mode=X if
the module can't load. Cache-buster bumped so cached copies pick up
the new behavior.
Importmap for @bitauth/libauth added to docs/, brand/, theseus/ so
the bundle's bare specifier resolves when register-flow.js is
lazy-loaded from those pages.
Verified end-to-end on the live docs page: all three dropdown items
open the modal inline at the correct step, no console errors, no
navigation.
817 lines
37 KiB
HTML
817 lines
37 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Portal — your names on Sirius.X</title>
|
||
<meta name="description" content="Sign in with your Bitcoin Cash seed phrase to see every BCNR name your wallet holds and manage records. Your keys never leave the browser.">
|
||
<link rel="icon" href="./assets/favicon.svg" type="image/svg+xml">
|
||
<link rel="mask-icon" href="./assets/favicon.svg" color="#d6ff3d">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:site_name" content="Sirius.X">
|
||
<meta property="og:title" content="Sirius.X portal — your BCNR names">
|
||
<meta property="og:description" content="Sign in with your seed phrase to see every name your wallet holds and manage records. Keys never leave the browser.">
|
||
<meta property="og:url" content="https://silentmode.st/sirius-x/portal.html">
|
||
<meta property="og:image" content="https://silentmode.st/sirius-x/brand/banner.svg">
|
||
<meta name="twitter:card" content="summary">
|
||
<style>
|
||
:root{
|
||
--bg:#0b0e14; --panel:#141a24; --panel2:#18202c; --line:rgba(255,255,255,.09);
|
||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
||
--ok:#4fd1a5; --taken:#f6768a; --warn:#ffc75f;
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:radial-gradient(1100px 560px at 50% -12%,#16202e,var(--bg));
|
||
color:var(--ink);font:16px/1.65 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh}
|
||
a{color:var(--acid)}
|
||
.wrap{max-width:820px;margin:0 auto;padding:0 1.2rem}
|
||
.topnav{position:sticky;top:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap;
|
||
padding:10px 1.2rem;background:rgba(11,14,20,.82);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
|
||
.topnav .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:14px;font-size:15px;letter-spacing:.2px}
|
||
.topnav .brand .g{color:var(--acid)}
|
||
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px}
|
||
.topnav a:hover{color:var(--ink);background:var(--panel)}
|
||
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)}
|
||
.topnav a.portal{margin-left:auto;background:rgba(214,255,61,.10);color:var(--acid);font-weight:500}
|
||
.topnav a.portal:hover{background:rgba(214,255,61,.18)}
|
||
header.hero{text-align:center;padding:3rem 1.2rem 1rem}
|
||
.mark{font-size:44px;line-height:1}
|
||
h1{font-size:clamp(1.6rem,4.5vw,2.2rem);margin:.3rem 0 .3rem}
|
||
h1 .g{color:var(--acid)}
|
||
.tag{color:var(--mut);font-size:1rem;max-width:580px;margin:0 auto}
|
||
h2{font-size:1.25rem;margin:0 0 .4rem}
|
||
section{padding:2rem 0;border-top:1px solid var(--line);margin-top:2rem}
|
||
section:first-of-type{border-top:none;margin-top:1rem}
|
||
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin-top:1rem}
|
||
label{display:block;font-size:13px;color:var(--mut);margin:14px 0 5px}
|
||
textarea,input[type=password],input[type=text]{
|
||
width:100%;padding:11px 13px;border-radius:10px;border:1px solid #ffffff22;background:#0e131b;
|
||
color:var(--ink);font-size:14px;font-family:inherit;outline:none
|
||
}
|
||
textarea{min-height:74px;resize:vertical;font-family:ui-monospace,monospace;font-size:13px}
|
||
textarea:focus,input:focus{border-color:#4b7bec}
|
||
.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}
|
||
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
|
||
.btn.acid{background:var(--acid);color:#0b0e14;font-weight:600}
|
||
.btn.small{padding:6px 12px;font-size:12.5px}
|
||
.btn:hover{filter:brightness(1.12)}
|
||
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
|
||
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px;align-items:center}
|
||
.muted{color:var(--mut);font-size:13px}
|
||
.dim{color:var(--dim);font-size:12.5px}
|
||
code,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px;word-break:break-all}
|
||
.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:999px;font-weight:600}
|
||
.b-x{background:rgba(214,255,61,.14);color:var(--acid)}
|
||
.b-warn{background:rgba(255,199,95,.14);color:var(--warn)}
|
||
.err{color:var(--taken);font-size:13.5px;margin-top:10px}
|
||
.status{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 14px;font-size:13px;color:var(--mut);margin-top:12px}
|
||
.status.ok{border-left:3px solid var(--ok)}
|
||
.status.err{border-left:3px solid var(--taken);color:var(--taken)}
|
||
.hidden{display:none}
|
||
.authbox{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
|
||
.authbox .card{margin:0}
|
||
.authbox h3{margin:0 0 .3rem;font-size:1rem}
|
||
.authbox p{margin:0;color:var(--mut);font-size:13px}
|
||
.authbox .card > .cta{margin-top:14px}
|
||
.name-row{display:grid;grid-template-columns:1fr auto auto auto;gap:10px;align-items:center;
|
||
padding:14px 16px;background:var(--panel2);border:1px solid var(--line);border-radius:12px;margin-top:8px}
|
||
.name-row .n{font-family:ui-monospace,monospace;font-size:15px}
|
||
.name-row .rec{color:var(--mut);font-family:ui-monospace,monospace;font-size:12px;grid-column:1/-1;margin-top:6px;word-break:break-all}
|
||
.name-row .rec .k{color:var(--dim)}
|
||
/* record editor modal */
|
||
.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:50}
|
||
.modal.open{display:flex}
|
||
.sheet{background:var(--panel);border:1px solid var(--line);border-radius:16px;max-width:620px;width:100%;
|
||
padding:24px 26px;position:relative}
|
||
.sheet h3{margin:0 0 .3rem;font-size:1.15rem;font-family:ui-monospace,monospace}
|
||
.sheet .sub{color:var(--mut);font-size:13px;margin:0 0 1.2rem}
|
||
.sheet .x{position:absolute;top:14px;right:16px;background:none;border:none;color:var(--dim);font-size:22px;cursor:pointer;line-height:1}
|
||
.sheet label{display:block;font-size:12px;color:var(--mut);margin:12px 0 4px;text-transform:uppercase;letter-spacing:.4px}
|
||
.sheet .field-hint{color:var(--dim);font-size:11.5px;margin-left:8px;text-transform:none;letter-spacing:0}
|
||
.sheet input[type=text],.sheet textarea{
|
||
width:100%;padding:9px 12px;border-radius:8px;border:1px solid #ffffff22;background:#0e131b;
|
||
color:var(--ink);font-size:13px;font-family:ui-monospace,monospace;outline:none
|
||
}
|
||
.sheet textarea{min-height:60px;resize:vertical}
|
||
.sheet input:focus,.sheet textarea:focus{border-color:#4b7bec}
|
||
.sheet .budget{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--mut);margin-top:14px;padding:8px 12px;background:var(--panel2);border-radius:8px}
|
||
.sheet .budget.over{background:rgba(246,118,138,.15);color:var(--taken)}
|
||
.sheet .actions{display:flex;gap:10px;justify-content:flex-end;margin-top:16px}
|
||
.sheet .steps-log{margin-top:12px;font-family:ui-monospace,monospace;font-size:12px;color:var(--mut);background:#0e131b;border:1px solid var(--line);border-radius:8px;padding:10px 12px;max-height:120px;overflow-y:auto;display:none}
|
||
.sheet .steps-log.on{display:block}
|
||
.sheet .steps-log div::before{content:"› ";color:var(--acid)}
|
||
.sheet .msg{font-size:13px;margin-top:12px}
|
||
.sheet .msg.err{color:var(--taken)} .sheet .msg.ok{color:var(--ok)}
|
||
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
|
||
@media (max-width:520px){
|
||
.name-row{grid-template-columns:1fr auto}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="topnav">
|
||
<a class="brand" href="./">⭐ <span class="g">Sirius.X</span></a>
|
||
<a href="./#try">Try it</a>
|
||
<a href="./#search-input">🪪 Register</a>
|
||
<a href="./tld.html">🌐 TLD</a>
|
||
<a href="./theseus/"><svg width="14" height="14" viewBox="0 0 64 64" style="vertical-align:-2px;margin-right:4px" aria-hidden="true"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/><path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/><circle cx="32" cy="32" r="3" fill="#0b0e14"/></svg>Theseus</a>
|
||
<a href="./portal.html" class="portal here">🔑 Sign in</a>
|
||
</nav>
|
||
|
||
<header class="hero">
|
||
<div class="mark">🔑</div>
|
||
<h1>Your <span class="g">names</span> portal</h1>
|
||
<p class="tag">Sign in with your Bitcoin Cash seed phrase or connect an external wallet to see
|
||
every BCNR name your wallet holds. Your keys never leave this browser tab.</p>
|
||
</header>
|
||
|
||
<div class="wrap">
|
||
|
||
<section id="signin">
|
||
<h2>Sign in</h2>
|
||
<p class="muted">Pick one. Nothing is transmitted — resolution is a read-only chain query,
|
||
and signing (for record edits) happens locally in the browser or in your external wallet.</p>
|
||
<div class="authbox" style="margin-top:14px">
|
||
<div class="card" id="card-new">
|
||
<h3>🆕 New wallet</h3>
|
||
<p>Generate a fresh 12-word BIP-39 phrase in your browser. Write it down before continuing —
|
||
it never leaves this page and there is nothing to reset if you lose it.</p>
|
||
<div class="row">
|
||
<button class="btn acid" id="new-btn">Generate a phrase</button>
|
||
</div>
|
||
<div id="new-phrase" class="hidden" style="margin-top:14px">
|
||
<label>Your new recovery phrase</label>
|
||
<textarea id="new-seed" readonly style="font-family:ui-monospace,monospace;font-size:14px"></textarea>
|
||
<label style="margin-top:10px;display:flex;gap:9px;align-items:flex-start;font-size:13.5px">
|
||
<input type="checkbox" id="new-ack" style="margin-top:3px">
|
||
<span>I have written this phrase down somewhere safe. I understand it is the only way
|
||
to recover this wallet and cannot be reset.</span>
|
||
</label>
|
||
<div class="row">
|
||
<button class="btn acid" id="new-signin-btn" disabled>Sign in with new wallet →</button>
|
||
<button class="btn ghost" id="new-copy-btn">Copy phrase</button>
|
||
</div>
|
||
</div>
|
||
<div id="new-err" class="err hidden"></div>
|
||
</div>
|
||
<div class="card" id="card-import">
|
||
<h3>📥 Import a seed phrase</h3>
|
||
<p>Paste your 12- or 24-word BIP-39 recovery phrase. It stays in memory only, wiped when you
|
||
close the tab or sign out.</p>
|
||
<label for="seed">Recovery phrase</label>
|
||
<textarea id="seed" placeholder="word1 word2 … word12" spellcheck="false" autocomplete="off"></textarea>
|
||
<div class="row">
|
||
<button class="btn acid" id="signin-btn">Sign in →</button>
|
||
<span class="dim">Chipnet only for now.</span>
|
||
</div>
|
||
<div id="signin-err" class="err hidden"></div>
|
||
</div>
|
||
<div class="card" id="card-wc">
|
||
<h3>🔗 Connect an external wallet</h3>
|
||
<p>Use Cashonize 0.9+ or Paytaca via WizardConnect. The wallet signs record edits — the seed
|
||
never touches this page.</p>
|
||
<div class="row">
|
||
<button class="btn acid" id="wc-btn">Connect wallet</button>
|
||
</div>
|
||
<div id="wc-uri-box" class="hidden" style="margin-top:14px">
|
||
<label>Open your wallet, scan or paste this URI, then approve the connection:</label>
|
||
<div style="display:flex;gap:8px;align-items:center;margin-top:6px">
|
||
<code id="wc-uri" style="flex:1;word-break:break-all;background:#0e131b;border:1px solid var(--line);border-radius:8px;padding:9px 11px;font-family:ui-monospace,monospace;font-size:12.5px;color:var(--ink)"></code>
|
||
<button class="btn ghost" id="wc-copy" type="button">Copy</button>
|
||
</div>
|
||
<p class="dim" style="margin-top:8px">Waiting for your wallet…</p>
|
||
<div class="row"><button class="btn ghost" id="wc-cancel" type="button">Cancel</button></div>
|
||
</div>
|
||
<div id="wc-err" class="err hidden"></div>
|
||
<p class="dim" style="margin-top:8px">Record edits and TLD mints are approved on your device
|
||
via the wallet's signing prompt — the seed never touches this page.</p>
|
||
</div>
|
||
</div>
|
||
<div class="status" style="margin-top:14px">
|
||
Don't have a wallet? Create one during
|
||
<a href="./#search-input">name registration</a> — the recovery phrase
|
||
is generated in your browser and encrypted with your password.
|
||
</div>
|
||
</section>
|
||
|
||
<section id="me" class="hidden">
|
||
<h2>Signed in</h2>
|
||
|
||
<div class="card" id="tld-register-card">
|
||
<h3 style="margin:0 0 .3rem;font-size:1.05rem">Register a new TLD</h3>
|
||
<p class="muted" style="margin:0 0 .8rem">TLDs are per-registry NFTs on the chipnet TLD beacon.
|
||
Public mints go through a service-fee output to fund the registry. Chipnet uses a placeholder
|
||
rate; mainnet will price by label length via a real oracle.</p>
|
||
<label>Label <span class="dim" style="margin-left:6px">1–16 chars, [a-z0-9-]</span></label>
|
||
<input type="text" id="tld-label" placeholder="myregistry" autocomplete="off" spellcheck="false" maxlength="16">
|
||
<label style="margin-top:12px">Service fee (USD) <span class="dim" style="margin-left:6px">chipnet test rate</span></label>
|
||
<input type="text" id="tld-usd" value="5">
|
||
<div class="status" id="tld-price">= <b>0</b> sats · rate <b>0</b> sats/USD</div>
|
||
<div class="row">
|
||
<button class="btn acid small" id="tld-check" disabled>Check availability</button>
|
||
<button class="btn small" id="tld-submit" disabled>Register (~0 sats)</button>
|
||
</div>
|
||
<div id="tld-log" class="status" style="display:none;font-family:ui-monospace,monospace;font-size:12.5px;margin-top:12px"></div>
|
||
<div id="tld-msg" style="margin-top:10px;font-size:13.5px"></div>
|
||
</div>
|
||
|
||
<h2 style="margin-top:2rem">Wallet</h2>
|
||
<div class="card">
|
||
<label>Receive address</label>
|
||
<div><code id="me-addr">—</code></div>
|
||
<label style="margin-top:14px">Token address (holds your name certificates)</label>
|
||
<div><code id="me-taddr">—</code></div>
|
||
<div class="row">
|
||
<button class="btn ghost small" id="signout-btn">Sign out</button>
|
||
<span class="dim" id="me-info">—</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2 style="margin-top:2rem">Names you hold</h2>
|
||
<div class="status" id="names-status">Loading names from the chain…</div>
|
||
<div id="names-list"></div>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<!-- record-editor modal — populated by JS when a name's Edit button fires -->
|
||
<div class="modal" id="editor">
|
||
<div class="sheet">
|
||
<button class="x" id="ed-close" title="close">×</button>
|
||
<h3 id="ed-name">—</h3>
|
||
<p class="sub">Edit records. Only your key can sign this UPD; the transaction fee is a few
|
||
cents in chain fees plus the beacon dust. Leave a field blank to omit that record.</p>
|
||
|
||
<label>h <span class="field-hint">inline HTML — served as-is at the site root</span></label>
|
||
<textarea id="ed-h" placeholder="<h1>My site</h1>"></textarea>
|
||
|
||
<label>s3 <span class="field-hint">Sia bucket key, trailing slash for directories</span></label>
|
||
<input type="text" id="ed-s3" placeholder="bns/myname/">
|
||
|
||
<label>ip <span class="field-hint">IPv4 that serves this name over Host header</span></label>
|
||
<input type="text" id="ed-ip" placeholder="1.2.3.4">
|
||
|
||
<label>tls <span class="field-hint">SHA-256 of your leaf cert — chain-pinned TLS trust</span></label>
|
||
<input type="text" id="ed-tls" placeholder="hex — 64 chars, no separators">
|
||
|
||
<label>u <span class="field-hint">redirect target</span></label>
|
||
<input type="text" id="ed-u" placeholder="https://…">
|
||
|
||
<div class="budget" id="ed-budget">payload: <b><span id="ed-budget-used">0</span></b> / <span id="ed-budget-max">?</span> bytes</div>
|
||
|
||
<div class="steps-log" id="ed-log"></div>
|
||
<div class="msg" id="ed-msg"></div>
|
||
|
||
<div class="actions">
|
||
<button class="btn ghost" id="ed-cancel">Cancel</button>
|
||
<button class="btn acid" id="ed-save">Save records →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer id="site-footer"></footer>
|
||
|
||
<script type="importmap">
|
||
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
|
||
</script>
|
||
|
||
<script type="module">
|
||
// The registrar bundle exports everything we need: BuiltInWallet for local
|
||
// wallet unlock, resolveName/buildIndex for chain queries, getUtxosForAddresses
|
||
// for holdings. This portal is a read-first surface; record edits (UPD) will
|
||
// come next and will piggyback on the same signing path the registrar uses.
|
||
// Cache-buster bumped whenever the bundle changes. Chrome's in-memory ES
|
||
// module map ignores Cache-Control on the .js URL for the lifetime of a tab,
|
||
// so a plain "/js/bns-register.js" import can return a stale instance days
|
||
// after deploy. A query string forces the module loader to treat this as a
|
||
// new URL and fetch fresh bytes.
|
||
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260907tldext";
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
||
|
||
let wallet = null;
|
||
|
||
// Persist signed-in state so the profile dropdown (profile-menu.js) can flip
|
||
// from "onboarding" to "signed in" and any tab on the same origin (sirius.x)
|
||
// reflects the current session. The wallet keys themselves never leave this
|
||
// page — localStorage only holds a small address-only breadcrumb, no seed.
|
||
function writeProfile(w) {
|
||
try {
|
||
localStorage.setItem("siriusProfile", JSON.stringify({
|
||
address: w.address,
|
||
tokenAddress: w.tokenAddress,
|
||
source: w.source ?? "seed", // "seed" | "wc"
|
||
signedInAt: Date.now(),
|
||
}));
|
||
} catch {}
|
||
// profile-menu.js listens for this to re-render the nav dropdown in the
|
||
// same tab (the "storage" event only fires in *other* tabs).
|
||
window.dispatchEvent(new Event("siriusProfileChanged"));
|
||
}
|
||
function clearProfile() {
|
||
try { localStorage.removeItem("siriusProfile"); } catch {}
|
||
window.dispatchEvent(new Event("siriusProfileChanged"));
|
||
}
|
||
|
||
// ---------- sign in ----------
|
||
$("signin-btn").addEventListener("click", async () => {
|
||
const btn = $("signin-btn");
|
||
const errBox = $("signin-err");
|
||
errBox.classList.add("hidden");
|
||
const raw = $("seed").value.trim();
|
||
if (!raw) { errBox.textContent = "Enter your seed phrase to sign in."; errBox.classList.remove("hidden"); return; }
|
||
btn.disabled = true; btn.textContent = "Unlocking…";
|
||
try {
|
||
wallet = await BNS.BuiltInWallet.fromMnemonic(raw);
|
||
} catch (e) {
|
||
errBox.textContent = "Could not unlock: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
btn.disabled = false; btn.textContent = "Sign in →";
|
||
return;
|
||
}
|
||
$("seed").value = "";
|
||
btn.disabled = false; btn.textContent = "Sign in →";
|
||
writeProfile(wallet);
|
||
await enterPortal();
|
||
});
|
||
|
||
// ---------- new wallet ----------
|
||
// Generates a fresh BIP-39 phrase in the browser via BuiltInWallet.create().
|
||
// The phrase is shown once for the user to write down; sign-in only unlocks
|
||
// after they acknowledge they have saved it. From that point on the flow is
|
||
// identical to import (BuiltInWallet in memory, no persistence of the seed).
|
||
$("new-btn").addEventListener("click", async () => {
|
||
const btn = $("new-btn");
|
||
const errBox = $("new-err");
|
||
errBox.classList.add("hidden");
|
||
btn.disabled = true; btn.textContent = "Generating…";
|
||
let fresh;
|
||
try { fresh = await BNS.BuiltInWallet.create(); }
|
||
catch (e) {
|
||
errBox.textContent = "Could not generate a phrase: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
btn.disabled = false; btn.textContent = "Generate a phrase";
|
||
return;
|
||
}
|
||
wallet = fresh;
|
||
$("new-seed").value = fresh.mnemonic;
|
||
$("new-phrase").classList.remove("hidden");
|
||
btn.disabled = true; btn.textContent = "Phrase generated";
|
||
});
|
||
|
||
$("new-ack").addEventListener("change", (e) => {
|
||
$("new-signin-btn").disabled = !e.target.checked;
|
||
});
|
||
|
||
$("new-copy-btn").addEventListener("click", () => {
|
||
navigator.clipboard?.writeText($("new-seed").value);
|
||
});
|
||
|
||
$("new-signin-btn").addEventListener("click", async () => {
|
||
if (!wallet) return;
|
||
// Wipe the visible phrase immediately — it's in the wallet object now.
|
||
$("new-seed").value = "";
|
||
$("new-phrase").classList.add("hidden");
|
||
writeProfile(wallet);
|
||
await enterPortal();
|
||
});
|
||
|
||
// ---------- WizardConnect ----------
|
||
// Ports the WC flow from register.html into a sign-in surface. The portal
|
||
// doesn't need to mint — it only reads holdings and signs UPDs — so this is
|
||
// simpler than the register flow: open a session, show the URI/QR, wait for
|
||
// approval, wrap the returned addresses in a wallet-shaped object with a
|
||
// `source: "wc"` flag so downstream code (record editor, sign-out) can
|
||
// dispatch to the external-wallet path.
|
||
$("wc-btn").addEventListener("click", async () => {
|
||
const btn = $("wc-btn");
|
||
const errBox = $("wc-err");
|
||
const uriBox = $("wc-uri-box");
|
||
errBox.classList.add("hidden");
|
||
btn.disabled = true; btn.textContent = "Starting…";
|
||
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,
|
||
// libauth is only bundled once (in the main bundle); wizardconnect.js
|
||
// needs these pubkey/hash helpers passed through.
|
||
crypto: {
|
||
pubkeyToAddress: BNS.pubkeyToAddress,
|
||
pubkeyToLockingBytecode: BNS.pubkeyToLockingBytecode,
|
||
binToHex: BNS.binToHex,
|
||
txidOfHex: BNS.txidOfHex,
|
||
},
|
||
});
|
||
} catch (e) {
|
||
errBox.textContent = "Could not start WizardConnect: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
return;
|
||
}
|
||
const uri = session.qrUri ?? session.uri;
|
||
$("wc-uri").textContent = uri;
|
||
uriBox.classList.remove("hidden");
|
||
btn.textContent = "Waiting for wallet…";
|
||
|
||
try {
|
||
await session.ready;
|
||
} catch (e) {
|
||
errBox.textContent = "Wallet did not approve: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
uriBox.classList.add("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
try { await session.disconnect(); } catch {}
|
||
return;
|
||
}
|
||
|
||
let addresses;
|
||
try { addresses = await session.getAddresses(); }
|
||
catch (e) {
|
||
errBox.textContent = "Wallet returned no addresses: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
uriBox.classList.add("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
try { await session.disconnect(); } catch {}
|
||
return;
|
||
}
|
||
if (!addresses?.length) {
|
||
errBox.textContent = "The connected wallet exposed no addresses.";
|
||
errBox.classList.remove("hidden");
|
||
uriBox.classList.add("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
try { await session.disconnect(); } catch {}
|
||
return;
|
||
}
|
||
|
||
// Wallet-shaped wrapper — enough for holdings lookup and dispatching the
|
||
// WC path in the record editor. External wallets don't expose keyFor(),
|
||
// so any codepath that needs it (TLD mint via built-in path) is guarded
|
||
// by `wallet.source !== "wc"`.
|
||
wallet = {
|
||
source: "wc",
|
||
address: addresses[0],
|
||
tokenAddress: BNS.toTokenAddress(addresses[0]),
|
||
watchedAddresses: addresses,
|
||
session,
|
||
};
|
||
writeProfile(wallet);
|
||
uriBox.classList.add("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
await enterPortal();
|
||
});
|
||
|
||
$("wc-cancel").addEventListener("click", async () => {
|
||
if (wallet?.session) { try { await wallet.session.disconnect(); } catch {} }
|
||
$("wc-uri-box").classList.add("hidden");
|
||
$("wc-btn").disabled = false;
|
||
$("wc-btn").textContent = "Connect wallet";
|
||
});
|
||
|
||
$("wc-copy").addEventListener("click", () => {
|
||
navigator.clipboard?.writeText($("wc-uri").textContent);
|
||
});
|
||
|
||
$("signout-btn").addEventListener("click", async () => {
|
||
// Close any live WC session before dropping the reference.
|
||
if (wallet?.source === "wc" && wallet.session) {
|
||
try { await wallet.session.disconnect(); } catch {}
|
||
}
|
||
wallet = null;
|
||
clearProfile();
|
||
$("me").classList.add("hidden");
|
||
$("signin").classList.remove("hidden");
|
||
$("names-list").innerHTML = "";
|
||
$("names-status").textContent = "Loading names from the chain…";
|
||
// Reset the new-wallet card so a subsequent sign-in starts clean.
|
||
$("new-btn").disabled = false; $("new-btn").textContent = "Generate a phrase";
|
||
$("new-phrase").classList.add("hidden");
|
||
$("new-ack").checked = false;
|
||
$("new-signin-btn").disabled = true;
|
||
// Reset the WC card too.
|
||
$("wc-btn").disabled = false; $("wc-btn").textContent = "Connect wallet";
|
||
$("wc-uri-box").classList.add("hidden");
|
||
});
|
||
|
||
// ---------- ?mode= deep-link ----------
|
||
// The profile dropdown links here with ?mode=new|import|wc. Each mode
|
||
// AUTO-RUNS its flow the moment the page loads, so the dropdown option
|
||
// actually feels like it did the thing:
|
||
// new -> click Generate a phrase (shows the 12-word seed immediately)
|
||
// import -> focus the seed textarea (user pastes)
|
||
// wc -> click Connect wallet (starts the WC session + shows QR/URI)
|
||
// The matching card is highlighted in acid green so context is obvious.
|
||
(function handleMode() {
|
||
const mode = new URLSearchParams(location.search).get("mode");
|
||
if (!mode) return;
|
||
const id = { new: "card-new", import: "card-import", wc: "card-wc" }[mode];
|
||
const el = id && document.getElementById(id);
|
||
if (!el) return;
|
||
el.style.borderColor = "var(--acid)";
|
||
el.style.boxShadow = "0 0 0 3px rgba(214,255,61,.15)";
|
||
setTimeout(() => el.scrollIntoView({ behavior: "smooth", block: "center" }), 50);
|
||
setTimeout(() => { el.style.borderColor = ""; el.style.boxShadow = ""; }, 3000);
|
||
// Trigger the primary action for the mode.
|
||
setTimeout(() => {
|
||
if (mode === "new") $("new-btn")?.click();
|
||
else if (mode === "import") $("seed")?.focus();
|
||
else if (mode === "wc") $("wc-btn")?.click();
|
||
}, 200);
|
||
})();
|
||
|
||
async function enterPortal() {
|
||
$("signin").classList.add("hidden");
|
||
$("me").classList.remove("hidden");
|
||
$("me-addr").textContent = wallet.address;
|
||
$("me-taddr").textContent = wallet.tokenAddress;
|
||
$("me-info").textContent = "watched addresses: " + wallet.watchedAddresses.length;
|
||
// Initialise the TLD-register card now that the wallet is available.
|
||
if (typeof updateTldPrice === "function") { updateTldPrice(); updateTldButtons(); }
|
||
await loadNames();
|
||
}
|
||
|
||
// ---------- load names ----------
|
||
// Approach: derive the scripthash for each watched address locally (the
|
||
// wallet code already knows how), send them in one POST to
|
||
// /api/holdings, and render whatever the gateway returns. Doing the
|
||
// wallet-holdings query server-side is what makes this work on browsers
|
||
// stuck behind firewalls that block chipnet electrum ports (50004,
|
||
// 50011, 62002) — the API is same-origin HTTPS on port 443, and the
|
||
// gateway can reach the full electrums the browser can't.
|
||
async function loadNames() {
|
||
const status = $("names-status");
|
||
const list = $("names-list");
|
||
list.innerHTML = "";
|
||
status.className = "status";
|
||
status.textContent = "Preparing your addresses…";
|
||
try {
|
||
const addresses = wallet.watchedAddresses;
|
||
const scripthashes = addresses.map((a) => BNS.addressToScripthash(a));
|
||
status.textContent = `Asking the gateway about ${addresses.length} addresses…`;
|
||
const r = await fetch("https://silentmode.st/api/holdings", {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify({ scripthashes }),
|
||
});
|
||
if (!r.ok) {
|
||
const detail = await r.text().catch(() => "");
|
||
throw new Error(`API ${r.status}: ${detail.slice(0, 200)}`);
|
||
}
|
||
const { count, names: mine } = await r.json();
|
||
if (!count) {
|
||
status.className = "status";
|
||
status.textContent = "No BCNR names found for this wallet. If you just registered, wait a minute for confirmation and reload.";
|
||
return;
|
||
}
|
||
status.className = "status ok";
|
||
status.textContent = `You own ${count} name${count === 1 ? "" : "s"}.`;
|
||
for (const e of mine) {
|
||
const div = document.createElement("div");
|
||
div.className = "name-row";
|
||
const records = e.records && Object.keys(e.records).length
|
||
? Object.entries(e.records).map(([k, v]) => `<span class="k">${esc(k)}=</span>${esc(String(v).slice(0, 80))}`).join(" · ")
|
||
: '<span class="k">(no records set)</span>';
|
||
const recJson = esc(JSON.stringify(e.records || {}));
|
||
div.innerHTML =
|
||
`<div class="n">${esc(e.name)}</div>` +
|
||
'<span class="badge b-x">on chain</span>' +
|
||
`<a class="btn small ghost" href="https://silentmode.st/bns/${encodeURIComponent(e.name)}/" target="_blank" rel="noopener">view →</a>` +
|
||
`<button class="btn small acid" data-edit="${esc(e.name)}" data-records='${recJson}'>Edit</button>` +
|
||
`<div class="rec">${records}</div>`;
|
||
list.appendChild(div);
|
||
}
|
||
// Wire Edit buttons after render.
|
||
list.querySelectorAll("[data-edit]").forEach((btn) => {
|
||
btn.onclick = () => openEditor(btn.getAttribute("data-edit"), JSON.parse(btn.getAttribute("data-records") || "{}"));
|
||
});
|
||
} catch (e) {
|
||
status.className = "status err";
|
||
status.textContent = "Could not load names: " + (e.message || e);
|
||
}
|
||
}
|
||
|
||
// ---------- record editor ----------
|
||
// Modal that composes a UPD payload from a small form and signs+broadcasts
|
||
// with the same code path register.html uses (BNS.setRecordsWithBuiltInWallet).
|
||
// External-wallet (WizardConnect) support piggybacks the same adapter and is a
|
||
// follow-up — the built-in path is the one this session's wallet flow can already
|
||
// sign with.
|
||
const EDITABLE_KEYS = ["h", "s3", "ip", "tls", "u"];
|
||
let editorName = null;
|
||
|
||
function openEditor(name, records) {
|
||
editorName = name;
|
||
$("ed-name").textContent = name;
|
||
for (const k of EDITABLE_KEYS) $("ed-" + k).value = records[k] || "";
|
||
$("ed-msg").textContent = "";
|
||
$("ed-msg").className = "msg";
|
||
$("ed-log").className = "steps-log";
|
||
$("ed-log").innerHTML = "";
|
||
$("ed-save").disabled = false;
|
||
$("ed-save").textContent = "Save records →";
|
||
updateBudget();
|
||
$("editor").classList.add("open");
|
||
}
|
||
function closeEditor() {
|
||
$("editor").classList.remove("open");
|
||
editorName = null;
|
||
}
|
||
$("ed-close").addEventListener("click", closeEditor);
|
||
$("ed-cancel").addEventListener("click", closeEditor);
|
||
$("editor").addEventListener("click", (e) => { if (e.target === $("editor")) closeEditor(); });
|
||
|
||
// Bytes budget: BNS payload is capped at 200 bytes total, of which the envelope
|
||
// (op + name) uses some. Show the user how many bytes their records take vs the
|
||
// remaining budget, and disable Save if they've gone over.
|
||
function collectRecords() {
|
||
const r = {};
|
||
for (const k of EDITABLE_KEYS) {
|
||
const v = $("ed-" + k).value.trim();
|
||
if (v) r[k] = v;
|
||
}
|
||
return r;
|
||
}
|
||
function updateBudget() {
|
||
if (!editorName) return;
|
||
try {
|
||
const max = BNS.payloadBudget(editorName, "UPD");
|
||
// The records object encodes to JSON inside the payload. Rough size:
|
||
// JSON.stringify(records).length gives a close-enough client-side estimate
|
||
// (the actual envelope is composed by the bundle at send time).
|
||
const used = new TextEncoder().encode(JSON.stringify(collectRecords())).length;
|
||
$("ed-budget-max").textContent = max;
|
||
$("ed-budget-used").textContent = used;
|
||
const over = used > max;
|
||
$("ed-budget").classList.toggle("over", over);
|
||
$("ed-save").disabled = over;
|
||
} catch { /* budget helper not available yet — ignore */ }
|
||
}
|
||
for (const k of EDITABLE_KEYS) $("ed-" + k).addEventListener("input", updateBudget);
|
||
|
||
$("ed-save").addEventListener("click", async () => {
|
||
if (!editorName || !wallet) return;
|
||
const records = collectRecords();
|
||
const msg = $("ed-msg");
|
||
const log = $("ed-log");
|
||
log.className = "steps-log on";
|
||
log.innerHTML = "";
|
||
const push = (t) => { const d = document.createElement("div"); d.textContent = t; log.appendChild(d); log.scrollTop = log.scrollHeight; };
|
||
msg.textContent = ""; msg.className = "msg";
|
||
$("ed-save").disabled = true;
|
||
$("ed-save").textContent = "Signing…";
|
||
let el = null;
|
||
try {
|
||
el = await BNS.connect();
|
||
push("connected to chipnet");
|
||
const res = wallet.source === "wc"
|
||
? await BNS.setRecordsWithExternalWallet(el, {
|
||
session: wallet.session, name: editorName, records,
|
||
onProgress: (step) => push(step),
|
||
})
|
||
: await BNS.setRecordsWithBuiltInWallet(el, {
|
||
wallet, name: editorName, records,
|
||
onProgress: (step) => push(step),
|
||
});
|
||
push("broadcast: " + (res.txid || res.txId || "(no txid)"));
|
||
msg.className = "msg ok";
|
||
msg.textContent = "Saved. Records will refresh on the chain within a block.";
|
||
$("ed-save").textContent = "Saved ✓";
|
||
// Refresh the names list in the background so the user sees the new
|
||
// records once they land (may take a few seconds via cache/mempool).
|
||
setTimeout(() => loadNames(), 2500);
|
||
} catch (e) {
|
||
msg.className = "msg err";
|
||
msg.textContent = "Failed: " + (e.message || e);
|
||
push("error: " + (e.message || e));
|
||
$("ed-save").disabled = false;
|
||
$("ed-save").textContent = "Save records →";
|
||
} finally {
|
||
try { el?.close?.(); } catch {}
|
||
}
|
||
});
|
||
|
||
// ---------- TLD register (chipnet MVP) ----------
|
||
// Simple oracle: static chipnet rate for testing. Replace with a live feed
|
||
// (CoinGecko, Kraken, or a Chainlink adapter) before mainnet. The `sats` output
|
||
// is what actually gets locked into a service-fee output on the TREG transaction.
|
||
const CHIPNET_SATS_PER_USD = 250_000; // 1 BCH ≈ $400 test rate
|
||
function usdToSats(usd) {
|
||
const n = Number(String(usd).replace(/[^\d.]/g, "")) || 0;
|
||
return Math.max(0, Math.round(n * CHIPNET_SATS_PER_USD));
|
||
}
|
||
|
||
function updateTldPrice() {
|
||
const usd = $("tld-usd").value.trim() || "0";
|
||
const sats = usdToSats(usd);
|
||
$("tld-price").innerHTML = `= <b>${sats.toLocaleString()}</b> sats · rate <b>${CHIPNET_SATS_PER_USD.toLocaleString()}</b> sats/USD (chipnet)`;
|
||
$("tld-submit").textContent = `Register (~${sats.toLocaleString()} sats)`;
|
||
}
|
||
|
||
function validateTldLabel(label) {
|
||
return /^[a-z0-9-]{1,16}$/.test(label);
|
||
}
|
||
|
||
function updateTldButtons() {
|
||
const label = $("tld-label").value.trim().toLowerCase();
|
||
const valid = validateTldLabel(label);
|
||
$("tld-check").disabled = !valid || !wallet;
|
||
$("tld-submit").disabled = !valid || !wallet;
|
||
}
|
||
|
||
$("tld-label").addEventListener("input", () => {
|
||
// normalise as user types
|
||
$("tld-label").value = $("tld-label").value.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 16);
|
||
updateTldButtons();
|
||
});
|
||
$("tld-usd").addEventListener("input", updateTldPrice);
|
||
|
||
$("tld-check").addEventListener("click", async () => {
|
||
const label = $("tld-label").value.trim();
|
||
const msg = $("tld-msg"); msg.textContent = ""; msg.className = "";
|
||
try {
|
||
const r = await fetch("https://navigate.st/api/tlds");
|
||
if (!r.ok) throw new Error("api " + r.status);
|
||
const j = await r.json();
|
||
const taken = (j.tlds || []).some((t) => (t.tld || t) === label);
|
||
msg.className = taken ? "err" : "ok";
|
||
msg.textContent = taken ? `❌ .${label} is already registered` : `✅ .${label} is available`;
|
||
msg.style.color = taken ? "var(--taken)" : "var(--ok)";
|
||
} catch (e) {
|
||
msg.textContent = "could not check: " + (e.message || e);
|
||
msg.style.color = "var(--warn)";
|
||
}
|
||
});
|
||
|
||
$("tld-submit").addEventListener("click", async () => {
|
||
if (!wallet) return;
|
||
const label = $("tld-label").value.trim();
|
||
if (!validateTldLabel(label)) return;
|
||
const sats = BigInt(usdToSats($("tld-usd").value.trim() || "0"));
|
||
const msg = $("tld-msg"); msg.textContent = ""; msg.className = "";
|
||
const log = $("tld-log"); log.style.display = "block"; log.innerHTML = "";
|
||
const push = (t) => { const d = document.createElement("div"); d.textContent = "› " + t; log.appendChild(d); log.scrollTop = log.scrollHeight; };
|
||
$("tld-submit").disabled = true;
|
||
$("tld-submit").textContent = "Signing…";
|
||
let el = null;
|
||
try {
|
||
el = await BNS.connect();
|
||
push("connected to chipnet");
|
||
const serviceFee = {
|
||
address: BNS.REGISTRAR?.serviceFee?.address ?? null,
|
||
sats,
|
||
};
|
||
const res = wallet.source === "wc"
|
||
? await BNS.registerTldWithExternalWallet(el, {
|
||
session: wallet.session,
|
||
tld: label,
|
||
records: {},
|
||
serviceFee,
|
||
tldListUrl: "https://navigate.st/api/tlds",
|
||
onProgress: (step) => push(step),
|
||
})
|
||
: await BNS.registerTldWithBuiltInWallet(el, {
|
||
wallet,
|
||
tld: label,
|
||
records: {},
|
||
serviceFee,
|
||
tldListUrl: "https://navigate.st/api/tlds",
|
||
onProgress: (step) => push(step),
|
||
});
|
||
push("broadcast: " + (res.txid || res.txId || "(no txid)"));
|
||
msg.style.color = "var(--ok)";
|
||
msg.textContent = `✅ .${label} minted. Category ${(res.category || "").slice(0, 16)}…`;
|
||
$("tld-submit").textContent = "Registered ✓";
|
||
// Trigger a name-list refresh — new TLD cert is an NFT held by this wallet.
|
||
setTimeout(() => loadNames(), 2500);
|
||
} catch (e) {
|
||
msg.style.color = "var(--taken)";
|
||
const raw = String(e.message || e);
|
||
// The most common failure by far: wallet has no chipnet coins. Turn the
|
||
// opaque error into a nudge toward a faucet.
|
||
if (/wallet is empty/i.test(raw)) {
|
||
msg.innerHTML = `Failed: wallet is empty. Fund <code style="font-size:11.5px">${esc(wallet.address)}</code>
|
||
with chipnet coins first — free from
|
||
<a href="https://tbch.googol.cash/" target="_blank" rel="noopener">tbch.googol.cash</a> or
|
||
<a href="https://chipnet.bch.ninja/faucet" target="_blank" rel="noopener">chipnet.bch.ninja/faucet</a>.`;
|
||
} else {
|
||
msg.textContent = "Failed: " + raw;
|
||
}
|
||
push("error: " + raw);
|
||
$("tld-submit").disabled = false;
|
||
updateTldPrice(); // reset button label
|
||
} finally {
|
||
try { el?.close?.(); } catch {}
|
||
}
|
||
});
|
||
// Initial paint on page load so the card shows the price even before sign-in.
|
||
updateTldPrice();
|
||
</script>
|
||
<script defer src="./js/site-footer.js?v=20260907rel"></script>
|
||
<script defer src="./js/profile-menu.js?v=20260907inline"></script>
|
||
</body>
|
||
</html>
|