feat(sirius-x): Sirius.X brand rebrand, banner star-divider, WC on portal
Three things from one review pass:
1. Rebrand text 'sirius.x' -> 'Sirius.X' as the brand style across all
sirius.x pages (titles, meta, nav wordmark, headings, prose, footer).
Also flipped the pantheon headings to Title.X form: Silentmode.X,
Sirius.X, Theseus.X, Hephaestus.X, Prometheus.X, Helios.X, Hermes.X.
Domain literals, URLs, and code samples stay lowercase (sirius.x is
the actual on-chain name; only prose changes). Logo/avatar SVG
wordmarks and aria-labels updated to match.
2. Banner (brand/banner.svg) redesigned around a centered layout:
'Sirius.X' wordmark above, the star's horizontal arm extended
edge-to-edge as a divider line, tagline lines below. Left tip at
(0,315), right tip at (1200,315) so the arm crosses the full 1200px
banner width; vertical body compact around center. Old layout had
the star crammed into the right half and the wordmark on the left.
3. WizardConnect wired up on portal.html:
- The 'Connect wallet' button now opens a real WC session via
wizardconnect.js, shows the WIZ:// URI (copyable) and waits for
the wallet to approve.
- Successful connection builds a wallet-shaped wrapper with
source: 'wc' and a WC session for signing; siriusProfile now
carries the source so the nav dropdown can differentiate.
- Record editor dispatches to setRecordsWithExternalWallet when
source === 'wc' (bundle already ships the adapter), else stays
on setRecordsWithBuiltInWallet.
- TLD Register submit is gated off for WC users with a tooltip —
no external-wallet variant of registerTld is exported yet, so
TLD mint still needs a built-in wallet.
- Sign-out disconnects the WC session before dropping the wallet.
- toTokenAddress used to derive the tokenAddress for WC users so
the 'Signed in' card shows both forms.
2026-09-07 01:35:35 +02:00
<!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" > ≈ $1– 3 / 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=20260906fixlistunspent";
const API = "https://navigate.st/api/name/";
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[& < >"']/g, (c) => ({ "&":"& ","< ":"< ",">":"> ",'"':"" ","'":"' " }[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(); } });
// ---------- 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 & 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 = "<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)" : "";
};
$("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" > < / script >
< / body >
< / html >