sirius/register.html
Local Dev 1cca60c4df fix(sirius-x): profile-menu URLs, landing search, empty-wallet hint, drop Pantheon
Five UX fixes from a review pass:

1. profile-menu.js: dropdown links (New/Add/WC wallet, Admin) were
   absolute /sirius-x/portal.html paths. That's fine on silentmode.st
   but under the BCNR route (sirius.x/) the origin is different, so
   the gateway forwarded to Sia which returned 'NoSuchKey' XML. Now
   derive the base URL from the nav's own .portal anchor href — which
   is already set per page with the right relative path — so it works
   from the root, from subdirs, and under any BCNR gateway. Admin URL
   is derived from the same base.

2. Landing hero got a search input. Submitting it normalises the
   label ([a-z0-9-], 63 chars) and redirects to register.html?q=<label>.
   register.html now honours ?q= on load: prefills the input, triggers
   the parallel multi-TLD lookup, and scrolls into view. Single source
   of truth stays in register.html; the landing just hands it a query.

3. Portal TLD-mint error path now special-cases 'wallet is empty' and
   shows a friendlier message with the wallet's address and links to
   two chipnet faucets, so the fix is one click away instead of a
   guess.

4. Removed 'Pantheon' from every page's top nav — it's a section on
   the landing that anyone scrolling will discover, and keeping it in
   the nav crowded the bar.

5. Cache-buster ?v=20260907rel on the profile-menu.js script include
   across all 7 pages so browsers that cached the pre-fix version
   pick up the new one on next load.
2026-09-07 20:32:14 +02:00

875 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Register a name — Sirius.X</title>
<meta name="description" content="Own a domain name on the Bitcoin Cash blockchain. Permanent, no renewal, censorship-resistant. Search availability, mint the name certificate to your own wallet, and pick decentralized hosting.">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Sirius.X">
<meta property="og:title" content="Register a name on Bitcoin Cash">
<meta property="og:description" content="Search any label across every BCNR TLD. Mint a name certificate straight to your own wallet. No renewals, no registrar.">
<meta property="og:url" content="https://silentmode.st/sirius-x/register.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;
--chain:#d6ff3d; --sia:#b39ddb; --srv:#4fd1a5; --ok:#4fd1a5; --taken:#f6768a;
--warn:#ffc75f;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
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}
.wrap{max-width:920px;margin:0 auto;padding:0 1.2rem}
a{color:var(--acid)}
header{text-align:center;padding:4.5rem 1.2rem 1rem}
.mark{font-size:52px;line-height:1}
h1{font-size:clamp(1.9rem,5vw,2.7rem);margin:.4rem 0 .3rem}
h1 .g{color:var(--acid)}
.tag{color:var(--mut);font-size:1.05rem;max-width:600px;margin:0 auto}
/* search */
.search{max-width:620px;margin:2rem auto 0;padding:0 1.2rem}
.searchrow{display:flex;gap:8px}
.searchrow input{flex:1;padding:14px 18px;border-radius:12px;border:1px solid #ffffff22;
background:#141a24;color:var(--ink);font-size:17px;outline:none}
.searchrow input:focus{border-color:#4b7bec}
.searchrow select{padding:0 12px;border-radius:12px;border:1px solid #ffffff22;background:#141a24;color:var(--ink);font-size:15px}
.hint{margin-top:8px;font-size:12.5px}
.result{margin-top:14px;min-height:28px;font-size:15px;display:grid;gap:8px}
.result .card{display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;
background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 16px;min-height:52px}
.result .card.checking{opacity:.55}
.result .name{font-family:ui-monospace,monospace;font-size:17px;overflow-wrap:anywhere}
.result .card .btn{padding:8px 14px;font-size:13px}
.badge{font-size:12px;padding:2px 10px;border-radius:999px;font-weight:600;white-space:nowrap}
.b-ok{background:rgba(79,209,165,.15);color:var(--ok)} .b-taken{background:rgba(246,118,138,.15);color:var(--taken)}
.b-check{background:rgba(139,152,169,.14);color:var(--mut)}
.b-nogate{background:rgba(255,199,95,.14);color:var(--warn)}
@media (max-width:520px){
.result .card{grid-template-columns:1fr;gap:8px}
.result .card .btn{justify-self:start}
}
.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:hover{filter:brightness(1.12)}
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
.muted{color:var(--dim);font-size:13px}
section{padding:2.6rem 0;border-top:1px solid var(--line);margin-top:2.5rem}
section:first-of-type{border-top:none;margin-top:1.5rem}
h2{font-size:1.4rem;margin:0 0 .4rem}
.lede{color:var(--mut);margin:0 0 1.5rem;max-width:660px}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px}
.card h3{margin:0 0 .4rem;font-size:1.05rem;display:flex;align-items:center;gap:8px}
.card p{margin:0;color:var(--mut);font-size:13.5px}
.card .price{color:var(--acid);font-size:13px;margin-top:.6rem;font-weight:600}
.dot{width:9px;height:9px;border-radius:50%;display:inline-block}
.d-chain{background:var(--chain)} .d-sia{background:var(--sia)} .d-srv{background:var(--srv)}
.note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid);
border-radius:10px;padding:14px 18px;color:var(--mut);font-size:14px;margin-top:1.4rem}
.note b{color:var(--ink)}
.note.warn{border-left-color:var(--warn)}
.note.danger{border-left-color:var(--taken)}
ol.steps{counter-reset:s;list-style:none;padding:0;margin:0;display:grid;gap:10px}
ol.steps li{counter-increment:s;display:grid;grid-template-columns:30px 1fr;gap:14px;align-items:start;
background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:13px 16px}
/* The counter owns column 1; everything else stacks in column 2. Without the
explicit placement the <span> auto-flows onto row 2 of column 1 — the 30px
one — and the description renders one word per line. */
ol.steps li::before{content:counter(s);grid-column:1;grid-row:1;
display:grid;place-items:center;width:24px;height:24px;border-radius:50%;
background:rgba(214,255,61,.14);color:var(--acid);font-size:12.5px;font-weight:700}
ol.steps li > *{grid-column:2;min-width:0}
ol.steps b{color:var(--ink)} ol.steps span{color:var(--mut);font-size:14px}
code.inline,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px}
footer{border-top:1px solid var(--line);padding:2.4rem 0 3.5rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
/* ---- registration flow ---- */
.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:26px 28px 28px}
.sheet h3{margin:0 0 .2rem;font-size:1.25rem}
.sheet .sub{color:var(--mut);font-size:14px;margin:0 0 1.2rem}
.sheet label{display:block;font-size:13px;color:var(--mut);margin:14px 0 5px}
.sheet input[type=text],.sheet input[type=password],.sheet textarea,.sheet select{
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}
.sheet textarea{min-height:76px;resize:vertical;font-family:ui-monospace,monospace;font-size:13px}
.sheet input:focus,.sheet textarea:focus{border-color:#4b7bec}
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}
.row.end{justify-content:flex-end}
.x{position:absolute;top:14px;right:18px;background:none;border:none;color:var(--dim);font-size:22px;cursor:pointer}
.sheetwrap{position:relative;width:100%;max-width:620px}
.phrase{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}
.phrase span{background:#0e131b;border:1px solid var(--line);border-radius:8px;padding:8px 10px;
font-family:ui-monospace,monospace;font-size:13px}
.phrase span b{color:var(--dim);font-weight:400;margin-right:6px;font-size:11px}
.addr{display:flex;gap:8px;align-items:center;margin-top:8px}
.addr code{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}
table.price{width:100%;border-collapse:collapse;margin-top:12px;font-size:14px}
table.price td{padding:7px 0;border-bottom:1px solid var(--line);color:var(--mut)}
table.price td:last-child{text-align:right;font-family:ui-monospace,monospace;color:var(--ink)}
table.price tr.total td{border-bottom:none;color:var(--ink);font-weight:600;padding-top:12px}
table.price tr.total td:last-child{color:var(--acid)}
table.price td small{display:block;color:var(--dim);font-size:11.5px}
.steps-log{margin-top:14px;font-family:ui-monospace,monospace;font-size:12.5px;color:var(--mut);
background:#0e131b;border:1px solid var(--line);border-radius:10px;padding:12px 14px;max-height:180px;overflow-y:auto}
.steps-log div::before{content:" ";color:var(--acid)}
.err{color:var(--taken);font-size:13.5px;margin-top:12px}
.ok{color:var(--ok)}
.pill{display:inline-block;font-size:11px;padding:2px 8px;border-radius:999px;background:rgba(214,255,61,.13);
color:var(--acid);margin-left:8px;vertical-align:middle}
.pill.soon{background:rgba(255,255,255,.07);color:var(--dim)}
.wopt{display:flex;gap:14px;align-items:flex-start;background:var(--panel2);border:1px solid var(--line);
border-radius:12px;padding:14px 16px;cursor:pointer;text-align:left;width:100%;color:inherit;font:inherit;margin-top:10px}
.wopt:hover{border-color:#4b7bec}
.wopt:disabled{opacity:.45;cursor:not-allowed}
.wopt .ic{font-size:22px;line-height:1}
.wopt b{display:block;font-size:14.5px}
.wopt span{color:var(--mut);font-size:13px}
.chk{display:flex;gap:9px;align-items:flex-start;margin-top:14px;font-size:13.5px;color:var(--mut)}
.chk input{margin-top:3px}
</style>
<link rel="stylesheet" href="https://silentmode.st/shared.css">
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg">
<link rel="mask-icon" href="./assets/favicon.svg" color="#d6ff3d">
<style>
/* sirius.x nav override — /shared.css above sets a Silent Mode-flavoured
.topnav; these rules layer on top so the register page matches the rest of
sirius.x (portal, docs, brand, theseus). */
nav.topnav.sirius{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)}
nav.topnav.sirius .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:14px;font-size:15px;letter-spacing:.2px}
nav.topnav.sirius .brand .g{color:var(--acid)}
nav.topnav.sirius a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px}
nav.topnav.sirius a:hover{color:var(--ink);background:var(--panel)}
nav.topnav.sirius a.here{color:var(--acid);background:rgba(214,255,61,.08)}
nav.topnav.sirius a.portal{margin-left:auto;background:rgba(214,255,61,.10);color:var(--acid);font-weight:500}
nav.topnav.sirius a.portal:hover{background:rgba(214,255,61,.18)}
</style>
</head>
<body>
<nav class="topnav sirius">
<a class="brand" href="./"><span class="g">Sirius.X</span></a>
<a href="./#try">Try it</a>
<a href="./docs/">📚 Docs</a>
<a href="./theseus/">🧭 Theseus</a>
<a href="./register.html" class="here">🪪 Register</a>
<a href="./brand/">🎨 Brand</a>
<a href="https://silentmode.st/" rel="noopener">Silent Mode ↗</a>
<a href="./portal.html" class="portal">🔑 Sign in</a>
</nav>
<header>
<div class="mark">⛓️</div>
<h1>Own your <span class="g">name</span></h1>
<p class="tag">A domain name that lives on the Bitcoin Cash blockchain. You hold it — permanently,
with no renewal, that no registrar can revoke or seize.</p>
</header>
<div class="search">
<div class="searchrow">
<input id="q" placeholder="search a name…" spellcheck="false" autocomplete="off" autofocus>
</div>
<div class="hint muted">One search checks the name across every TLD. Available names appear first.</div>
<div class="result" id="result"><span class="muted">Type a name to check availability on the chain.</span></div>
</div>
<div class="wrap">
<section>
<h2>How registration works</h2>
<p class="lede">You are not buying a name from us — you are minting a name <b>certificate you own</b> on the
Bitcoin Cash chain. We provide the tool; the chain gives you the name.</p>
<ol class="steps">
<li><b>You hold the key.</b> <span>The name is a certificate on the chain — a title deed — controlled by your
wallet. Whoever holds the key owns the name: not us, not a registrar. It cannot be taken back.</span></li>
<li><b>You pay the chain, not a middleman.</b> <span>Registration is a single Bitcoin Cash transaction —
a few cents in network fees, plus a small service fee. No yearly renewal, ever.</span></li>
<li><b>You point it anywhere.</b> <span>Attach a record: store the site on the chain itself, on the Sia
network, or on any server you run. Update it any time — only your key can.</span></li>
</ol>
<div class="note"><b>Self-custody, by design.</b> Because you hold the key, only you can transfer or
change the name. That is the whole point — it also means <b>keep your recovery phrase safe</b>;
there is no company that can reset it for you. We never take custody of your name or your funds,
and your recovery phrase never leaves your browser.</div>
</section>
<section id="hosting">
<h2>Hosting — where your site lives</h2>
<p class="lede">Point your name at any of these. Mix and match; change it whenever you like.</p>
<div class="grid">
<div class="card">
<h3><span class="dot d-chain"></span> On the blockchain</h3>
<p>Tiny sites live <i>inside the transaction itself</i> — no server, no host, permanent as the chain.
Best for a page, a profile, a link hub.</p>
<div class="price">≈ free · a few cents once</div>
</div>
<div class="card">
<h3><span class="dot d-sia"></span> Sia network</h3>
<p>Full multi-file sites, encrypted and scattered across independent storage hosts worldwide.
No hosting company to bill or suspend you.</p>
<div class="price">≈ $13 / TB / month (a website: pennies)</div>
</div>
<div class="card">
<h3><span class="dot d-srv"></span> Your own server</h3>
<p>Already run a server or VPS? Point the name straight at it. Works with anything you host —
the name just resolves to your IP.</p>
<div class="price">your server's cost</div>
</div>
<div class="card">
<h3><span class="dot d-sia"></span> IPFS <span class="muted">(soon)</span></h3>
<p>Content-addressed peer-to-peer storage. The name pins a content hash; anyone can help serve it.
Coming as a record type.</p>
<div class="price">free + optional pinning</div>
</div>
</div>
</section>
<section>
<h2>Anonymous VPS — if you want your own server</h2>
<p class="lede">For the "your own server" option, or to run a node. Privacy-first providers that take
crypto and ask for no identity — consistent with owning a censorship-resistant name.</p>
<div class="grid">
<div class="card"><h3>MyNymBox</h3><p>No-KYC VPS, web hosting, and anonymous domain registration.
Pays in Monero / Bitcoin / Lightning. Free WHOIS privacy.</p><div class="price">from ~€3.50/mo</div></div>
<div class="card"><h3>Njalla</h3><p>Privacy-by-proxy domains and VPS — they hold the registration on your
behalf, you keep control. Crypto accepted.</p><div class="price">domains ~€15/yr</div></div>
<div class="card"><h3>1984 / others</h3><p>Iceland-based, privacy-respecting hosting. Several no-KYC
providers exist — pick one outside your jurisdiction, pay in Monero.</p><div class="price">varies</div></div>
</div>
<div class="note">These are independent third parties, listed for convenience — not affiliated with Silent
Mode. For a name you never have to renew and nobody can seize, the name itself lives on the chain; a VPS
only matters if you choose to host content on your own server.</div>
</section>
</div>
<footer>
Register a name on <b>Sirius.X</b> ·
<a href="./">back to sirius.x</a> ·
<a href="./docs/">docs</a> ·
<a href="./portal.html">portal</a><br>
Alpha on a test network (chipnet). You hold your own keys; verify everything against the chain.<br>
<span style="opacity:.8">Wallet connection via
<a href="https://gitlab.com/riftenlabs/lib/wizardconnect" target="_blank" rel="noopener">WizardConnect</a>
by Riften Labs —
<a href="https://silentmode.st/vendor/LICENSE.lgpl-3.0.txt">LGPL-3.0-or-later</a>
(<a href="https://silentmode.st/vendor/LICENSE.gpl-3.0.txt">GPL-3.0</a>);
the library ships as a separate, unmodified module at
<a href="https://silentmode.st/js/wizardconnect.js">/js/wizardconnect.js</a> so you can replace it.
Cryptography by <a href="https://github.com/bitauth/libauth" target="_blank" rel="noopener">libauth</a> (MIT).</span>
</footer>
<!-- One shared libauth for every bundle. The browser's module loader guarantees
its top-level-await WASM init settles before any importer runs — which a
per-bundle copy does not. See Argus/src/web/libauth-entry.js. -->
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script>
<!-- ───────────────────────── registration flow ───────────────────────── -->
<div class="modal" id="modal">
<div class="sheetwrap">
<button class="x" id="close" title="close">×</button>
<div class="sheet" id="sheet"></div>
</div>
</div>
<script type="module">
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260907tldext";
const API = "https://navigate.st/api/name/";
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c]));
const sats = (v) => Number(v).toLocaleString("en-US");
// ---------- availability search (multi-TLD) ----------
// The user types one label; we fan out to every TLD in TLDS in parallel and
// render results as they land, available first. Same lookup() call the previous
// single-TLD version used — API for speed, chain via BNS as authoritative
// fallback. The TLD list here is deliberately client-side: it mirrors the
// on-chain seed set (Decentralized.DNS/DESIGN-tld-registry.md §9) plus TLDs
// under active discussion. TLDs marked `onchain:false` show a "not-yet-a-TLD"
// warning so nobody registers a name under a TLD that isn't in the registry
// (the enforcement gate would reject it later).
const TLDS = [
{ tld: "bch", onchain: true },
{ tld: "p2p", onchain: true },
{ tld: "bit", onchain: true },
{ tld: "nav", onchain: true },
{ tld: "test", onchain: true },
{ tld: "x", onchain: true },
{ tld: "asm", onchain: true },
{ tld: "neo", onchain: true },
{ tld: "gt", onchain: true },
{ tld: "sc", onchain: true },
{ tld: "sia", onchain: true },
{ tld: "dex", onchain: true },
{ tld: "cex", onchain: true },
{ tld: "nt", onchain: true },
{ tld: "dea", onchain: true },
{ tld: "hub", onchain: true },
{ tld: "os", onchain: true },
{ tld: "vpn", onchain: true },
// .com is held by the operator on-chain but excluded from the public
// registrar UI (2026-08-29 decision) — the TLD certificate stays minted so
// nobody else can claim it, but no second-level names are offered.
];
let timer, lastReq = 0;
function clean(v){ return v.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0,32); }
// Availability, two ways. The API is a convenience (fast, cached); the chain is
// the authority. If the API is unreachable — down, blocked, censored — fall
// straight through to electrum, which is the same source the API reads. A name
// service that stops working when one HTTP host does would rather miss the point.
async function lookup(full) {
try {
const r = await fetch(API + encodeURIComponent(full));
if (!r.ok) throw new Error("api " + r.status);
return await r.json();
} catch {
const a = await BNS.checkAvailability(await client(), full);
return { name: a.name, registered: !a.available, records: a.entry?.records ?? {}, viaChain: true };
}
}
// One row per TLD. Cards start in "checking" state; each fetch replaces its own
// card as it resolves. Available cards are then moved to the top and Register
// buttons wired — this happens after every fetch so the ordering settles as
// results come in, and the user sees available names first without a full
// re-render flash.
function rowFor(tld, state, full, extra = "") {
const btnHtml = state === "ok"
? `<button class="btn acid" data-register="${esc(full)}">Register →</button>`
: state === "taken"
? `<a class="btn ghost" href="https://navigate.st/bns/${encodeURIComponent(full)}" target="_blank" rel="noopener">view →</a>`
: `<span></span>`;
const badge = state === "ok" ? '<span class="badge b-ok">available</span>'
: state === "taken" ? '<span class="badge b-taken">taken</span>'
: state === "nogate" ? '<span class="badge b-nogate">not on TLD registry yet</span>'
: state === "error" ? '<span class="badge b-check">check failed</span>'
: '<span class="badge b-check">checking…</span>';
return `<div class="card ${state === "checking" ? "checking" : ""}" data-tld="${esc(tld)}" data-state="${esc(state)}">`
+ `<span class="name">${esc(full)}</span>${badge}${btnHtml}${extra}</div>`;
}
function resort(res) {
// Available first, then taken/nogate/error/checking. Stable within groups.
const rank = { ok: 0, taken: 1, nogate: 2, error: 3, checking: 4 };
const cards = [...res.querySelectorAll(".card")].sort((a, b) => (rank[a.dataset.state] ?? 9) - (rank[b.dataset.state] ?? 9));
for (const c of cards) res.appendChild(c);
}
async function check() {
const label = clean($("q").value);
const res = $("result");
if (!label) { res.innerHTML = '<span class="muted">Type a name to check availability on the chain.</span>'; return; }
const my = ++lastReq;
// Seed the result grid with a checking card per TLD, in canonical order.
res.innerHTML = TLDS.map(({ tld }) => rowFor(tld, "checking", label + "." + tld)).join("");
// Fan out lookups. Update each card as its result arrives; re-sort after each.
await Promise.all(TLDS.map(async ({ tld, onchain }) => {
const full = label + "." + tld;
let state = "error";
if (!onchain) {
// The TLD is not on the TLD beacon (design's enforcement gate would
// reject any REG). Skip the API round-trip and mark clearly; wiring an
// available/taken lookup here would mislead the user into a doomed reg.
state = "nogate";
} else {
try {
const j = await lookup(full);
if (my !== lastReq) return; // stale keystroke
if (j.error) state = "error";
else state = j.registered ? "taken" : "ok";
} catch { state = "error"; }
}
if (my !== lastReq) return;
const card = res.querySelector(`.card[data-tld="${tld}"]`);
if (!card) return;
card.outerHTML = rowFor(tld, state, full);
resort(res);
// Rewire Register buttons after DOM churn.
res.querySelectorAll("[data-register]").forEach((b) => {
b.onclick = () => startFlow(b.getAttribute("data-register"));
});
}));
}
$("q").addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(check, 350); });
$("q").addEventListener("keydown", (e) => { if (e.key === "Enter") { clearTimeout(timer); check(); } });
// ---------- ?q= deep-link ----------
// The landing page has a search box that submits to register.html?q=<label>,
// so people can search from the sirius.x home without clicking through. If
// the URL carries a `q`, prefill the input and trigger the same lookup.
(() => {
const q = new URLSearchParams(location.search).get("q");
if (!q) return;
const clean = q.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 63);
if (!clean) return;
$("q").value = clean;
// Small delay so the results container is in the layout before we scroll.
setTimeout(() => { check(); $("q").scrollIntoView({ behavior: "smooth", block: "start" }); }, 100);
})();
// ---------- flow state ----------
const state = { name: null, wallet: null, session: null, client: null, quote: null, result: null };
const modal = $("modal"), sheet = $("sheet");
function open(){ modal.classList.add("open"); }
function close(){
modal.classList.remove("open");
if (state.client) { try { state.client.close(); } catch {} state.client = null; }
if (state.session) { try { state.session.disconnect(); } catch {} state.session = null; }
}
$("close").onclick = close;
modal.addEventListener("click", (e) => { if (e.target === modal) close(); });
function render(html){ sheet.innerHTML = html; }
function err(e){
sheet.querySelectorAll(".err").forEach((n) => n.remove()); // one error at a time, not a pile
const box = document.createElement("div");
box.className = "err";
box.textContent = e?.message ?? String(e);
sheet.appendChild(box);
}
async function client(){
if (!state.client) state.client = await BNS.connect();
return state.client;
}
function startFlow(name){
state.name = name;
open();
stepWallet();
}
// ---------- step 1: choose a wallet ----------
function stepWallet(){
const saved = BNS.BuiltInWallet.exists();
render(`
<h3>Register ${esc(state.name)}</h3>
<p class="sub">First, the wallet that will <b>own</b> the name. The certificate is minted
straight to it — we never hold it.</p>
${saved ? `<button class="wopt" id="w-unlock"><span class="ic">🔓</span><span>
<b>Unlock my browser wallet</b>
<span>You already created one on this device.</span></span></button>` : ``}
<button class="wopt" id="w-new"><span class="ic">✨</span><span>
<b>Create a new wallet<span class="pill">easiest</span></b>
<span>Made here in your browser. You get a recovery phrase to write down — it is the only key.</span></span></button>
<button class="wopt" id="w-import"><span class="ic">🔑</span><span>
<b>Import a recovery phrase</b>
<span>Restore a wallet you already have, from its 12-word phrase.</span></span></button>
<button class="wopt" id="w-wiz"><span class="ic">🪄</span><span>
<b>Connect a wallet — WizardConnect<span class="pill">most private</span></b>
<span>Cashonize 0.9+ or Paytaca. Encrypted end-to-end over Nostr; the relay sees only
ciphertext. Your keys never leave your wallet.</span></span></button>
<div class="note" style="margin-top:18px">Everything below runs on <b>chipnet</b>, Bitcoin Cash's
test network. The coins are free and worth nothing — this is an alpha, and it is the honest place
to try it before real money is involved.</div>
`);
if (saved) $("w-unlock").onclick = stepUnlock;
$("w-new").onclick = stepCreate;
$("w-import").onclick = stepImport;
$("w-wiz").onclick = () => stepExternal("wizard");
}
// ---------- external wallets (WizardConnect) ----------
// Takes the identical transaction the built-in path builds; only the signing
// transport differs. The buyer approves on their own device.
async function stepExternal(){
const label = "WizardConnect";
render(`<h3>Connect with ${esc(label)}</h3><p class="sub">Starting the connection…</p>`);
let session;
try {
const mod = await import("https://silentmode.st/js/wizardconnect.js");
session = await mod.openWizardSession({
dappName: BNS.REGISTRAR.wizardConnect.dappName,
dappIcon: BNS.REGISTRAR.wizardConnect.dappIcon,
prefix: BNS.REGISTRAR.addressPrefix,
// libauth lives in the main bundle only — see the note in wizardconnect.js
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 = stepWallet;
return;
}
const uri = session.qrUri ?? session.uri;
render(`
<h3>Open your wallet</h3>
<p class="sub">Scan or paste this into ${esc(label)}-capable wallet, then approve the connection.</p>
<div class="addr"><code id="uri">${esc(uri)}</code><button class="btn ghost" id="copy">Copy</button></div>
<div class="note warn" style="margin-top:16px">Waiting for your wallet…
The link stays valid until you close this page.</div>
<div class="row end"><button class="btn ghost" id="cancel">Cancel</button></div>
`);
$("copy").onclick = () => navigator.clipboard?.writeText(uri);
$("cancel").onclick = async () => { try { await session.disconnect(); } catch {} stepWallet(); };
try {
await session.ready;
} catch (e) {
err(e);
return;
}
stepExternalConfirm(session, label);
}
async function stepExternalConfirm(session, label){
render(`<h3>Connected</h3><p class="sub">Reading your wallet and building the registration…</p>`);
try {
const c = await client();
const addresses = await session.getAddresses();
const owner = addresses[0];
const balance = await BNS.getBalance(c, addresses);
const genesis = BNS.describeGenesisRequirement(balance.hasGenesisInput);
if (balance.sats === 0n) {
render(`<h3>That wallet is empty</h3>
<p class="sub">Fund <span class="mono">${esc(owner)}</span> with chipnet coins and reconnect.</p>
<div class="row end"><button class="btn ghost" id="back">Back</button></div>`);
$("back").onclick = async () => { try { await session.disconnect(); } catch {} stepWallet(); };
return;
}
const quote = await BNS.quoteRegistration(c, { name: state.name, ownerAddress: owner, records: {} });
const rows = BNS.priceSummary(quote.costs).map((l) => `
<tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td>
<td>${sats(l.sats)} sat</td></tr>`).join("");
render(`
<h3>Confirm ${esc(quote.displayName)}</h3>
<p class="sub">Connected to ${esc(label)}. The certificate is minted to
<span class="mono">${esc(owner)}</span> — your wallet, your key.</p>
<table class="price">${rows}</table>
${genesis.ready ? "" : `<div class="note warn" style="margin-top:16px">
<b>Two approvals needed.</b> ${esc(genesis.explanation)}</div>`}
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="go">Register — approve in your wallet</button></div>
`);
$("back").onclick = async () => { try { await session.disconnect(); } catch {} stepWallet(); };
$("go").onclick = async () => {
render(`<h3>Approve in your wallet</h3>
<p class="sub">${esc(label)} has sent the transaction to your device. Check the details there
and approve. Do not close this tab.</p><div class="steps-log" id="log"></div>`);
const log = $("log");
const say = (s) => { const d = document.createElement("div"); d.textContent = s; log.appendChild(d); };
try {
const res = await BNS.registerWithExternalWallet(await client(), {
session, name: state.name, ownerAddress: owner, records: {}, onProgress: say,
});
state.result = res;
state.session = session; // keep it open — the record step signs too
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);
}
}
// ---------- step 2a: create ----------
function stepCreate(){
render(`
<h3>Create your wallet</h3>
<p class="sub">Generated in this browser. The phrase is never sent anywhere — not to us, not to anyone.</p>
<label for="pw">Password (encrypts the wallet on this device)</label>
<input type="password" id="pw" autocomplete="new-password" placeholder="at least 8 characters">
<label for="pw2">Repeat password</label>
<input type="password" id="pw2" autocomplete="new-password">
<div class="note warn" style="margin-top:16px"><b>There is no reset.</b> The password protects this
device; the recovery phrase you will see next <i>is</i> the wallet. Lose both and the name is gone
forever — that is what self-custody means.</div>
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="next">Generate phrase →</button></div>
`);
$("back").onclick = stepWallet;
$("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 = stepFund;
}
// ---------- step 2b: import / unlock ----------
function stepImport(){
render(`
<h3>Import a recovery phrase</h3>
<p class="sub">12 words, separated by spaces. It stays in this browser.</p>
<label for="mn">Recovery phrase</label>
<textarea id="mn" spellcheck="false" placeholder="word word word …"></textarea>
<label for="pw">Password to encrypt it on this device</label>
<input type="password" id="pw" autocomplete="new-password">
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="next">Import →</button></div>
`);
$("back").onclick = stepWallet;
$("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;
stepFund();
} catch (e) { err(e); }
};
}
function stepUnlock(){
render(`
<h3>Unlock your wallet</h3>
<p class="sub">Decrypts the wallet stored in this browser.</p>
<label for="pw">Password</label>
<input type="password" id="pw" autocomplete="current-password">
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="next">Unlock →</button></div>
`);
$("back").onclick = stepWallet;
$("pw").onkeydown = (e) => { if (e.key === "Enter") $("next").click(); };
$("next").onclick = async () => {
try { state.wallet = await BNS.BuiltInWallet.load($("pw").value); stepFund(); }
catch (e) { err(e); }
};
}
// ---------- step 3: 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">${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);
}
// ---------- step 4: confirm ----------
async function stepConfirm(){
render(`<h3>Preparing…</h3><p class="sub">Building your registration transaction.</p>`);
try {
const c = await client();
const avail = await BNS.checkAvailability(c, state.name);
if (!avail.available) throw new Error(`"${avail.name}" was just registered by someone else`);
const quote = await BNS.quoteRegistration(c, {
name: state.name,
ownerAddress: state.wallet.address,
records: {},
});
state.quote = quote;
const rows = BNS.priceSummary(quote.costs).map((l) => `
<tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td>
<td>${sats(l.sats)} sat</td></tr>`).join("");
render(`
<h3>Confirm ${esc(quote.displayName)}</h3>
<p class="sub">One transaction. It mints the certificate to <b>your</b> address and pays the fees.</p>
<table class="price">${rows}</table>
<div class="note" style="margin-top:16px">The certificate will be minted to
<span class="mono">${esc(state.wallet.address)}</span> — your key, your name.
You can set where it points immediately afterwards.</div>
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="go">Register — sign &amp; broadcast</button></div>
`);
$("back").onclick = stepFund;
$("go").onclick = stepRegister;
} catch (e) {
render(`<h3>Could not prepare</h3><p class="sub">${esc(e.message)}</p>
<div class="row end"><button class="btn ghost" id="back">Back</button></div>`);
$("back").onclick = stepFund;
}
}
// ---------- step 5: register ----------
async function stepRegister(){
render(`<h3>Registering ${esc(state.name)}</h3>
<p class="sub">Signing in your browser and broadcasting to the chain. Do not close this tab.</p>
<div class="steps-log" id="log"></div>`);
const log = $("log");
const say = (s) => { const d = document.createElement("div"); d.textContent = s; log.appendChild(d); log.scrollTop = log.scrollHeight; };
const words = {
"checking-availability": "checking the name is still free",
"loading-coins": "loading your coins",
"preparing-wallet": "preparing your wallet (one-off setup transaction)",
"prepared": "wallet prepared",
"building": "building the registration transaction",
"signing": "signing with your key — in this browser",
"broadcasting": "broadcasting to the Bitcoin Cash network",
"registered": "registered",
};
try {
const res = await BNS.registerWithBuiltInWallet(await client(), {
wallet: state.wallet,
name: state.name,
records: {},
onProgress: (s) => say(words[s] ?? s),
});
state.result = res;
stepDone();
} catch (e) {
err(e);
const row = document.createElement("div");
row.className = "row end";
row.innerHTML = `<button class="btn ghost" id="back">Back</button>`;
sheet.appendChild(row);
$("back").onclick = stepConfirm;
}
}
// ---------- step 6: done + point it somewhere ----------
function stepDone(){
const r = state.result;
render(`
<h3><span class="ok">✓</span> ${esc(r.displayName)} is yours</h3>
<p class="sub">The certificate is on the chain, held by your key. Nobody can take it back.</p>
<table class="price">
<tr><td>Transaction</td><td>${esc(r.txid.slice(0,20))}…</td></tr>
<tr><td>Certificate ID</td><td>${esc(r.category.slice(0,20))}…</td></tr>
<tr><td>Owner</td><td>${esc(r.ownerAddress.slice(0,22))}…</td></tr>
</table>
<h3 style="margin-top:24px;font-size:1.05rem">Point it somewhere</h3>
<p class="sub">Optional, and changeable any time — only your key can.</p>
<label for="kind">Record type</label>
<select id="kind">
<option value="h">A tiny site stored on the chain itself (h)</option>
<option value="u">Redirect to a web address (u)</option>
<option value="ip">Your own server's IPv4 address (ip)</option>
<option value="tls">TLS certificate fingerprint (tls)</option>
</select>
<label for="val" id="vl">HTML — kept small; it lives inside the transaction</label>
<textarea id="val" spellcheck="false" placeholder="&lt;h1&gt;hello&lt;/h1&gt;"></textarea>
<div class="muted" id="budget"></div>
<div class="row end"><button class="btn ghost" id="skip">Done for now</button>
<button class="btn acid" id="set">Publish record</button></div>
`);
const labels = {
h: ["HTML — kept small; it lives inside the transaction", "<h1>hello</h1>"],
u: ["The https:// address the name should open", "https://example.org"],
ip: ["IPv4 address of your server", "203.0.113.9"],
tls: ["SHA-256 fingerprint of your TLS certificate (hex)", "a1b2c3…"],
};
const budget = () => {
const used = new TextEncoder().encode($("val").value).length;
const max = 200 - 30 - r.name.length;
$("budget").textContent = `${used} / ~${max} bytes used`;
$("budget").style.color = used > max ? "var(--taken)" : "";
};
$("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">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;
}
};
}
</script>
<script defer src="./js/profile-menu.js?v=20260907rel"></script>
</body>
</html>