feat(sirius-x): pencil Register icon + TLD mint via shared modal

Two UX polishes:

1. Register-tab icon flipped from 🪪 (identification-card emoji, renders
   as a hollow box in system-ui fonts without extended emoji) to ✏️
   (pencil), which reads as 'edit/create' and ships in every emoji font
   worth targeting. Changed across the nav on all 8 pages plus the
   footer injector's Product column. Kept the Register nav item — six
   items total in the nav, and from any subpage it's one click to the
   search on landing.

2. tld.html no longer duplicates the wallet-choice + mint flow inline.
   The Register button on a search result now hands the label off to
   the shared register-flow.js modal — same modal that name registration
   uses — via a new window.siriusRegisterTld(label, {serviceFeeSats})
   entry point.

   register-flow.js grew:
   - startTldFlow / window.siriusRegisterTld: sets state.tld and walks
     the modal through wallet-choice → Fund → stepConfirmTld → stepRegisterTld
     → stepDoneTld
   - stepConfirmTld: shows label + fee + owner, no per-name quote
   - stepRegisterTld: dispatches to registerTldWithBuiltInWallet or
     registerTldWithExternalWallet based on state.wallet/state.session
   - stepDoneTld: 'X is yours' with txid + certificate id + operator
     address, no record editor (a TLD certificate has no records to
     set immediately)
   - stepWallet / stepExternalConfirm branch on state.tld so the modal
     heading and the WC-confirm path route correctly

   tld.html trimmed: removed the entire signin-section + mint-section
   plus their inline handlers (~250 lines gone). Now just search +
   registered-TLDs list + a delegator to the shared modal. If a user
   is already signed in via the wallet dropdown, the modal recognises
   the saved wallet ('Unlock my browser wallet' button appears) instead
   of asking them to sign in again on this page.
This commit is contained in:
Local Dev 2026-09-08 00:10:11 +02:00
parent 5e0c971c23
commit bce4aa3529
9 changed files with 224 additions and 451 deletions

View file

@ -82,7 +82,7 @@
<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="../#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="./" class="here">🛠 Admin</a>

View file

@ -81,7 +81,7 @@
<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="../#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">🔑 Sign in</a>

View file

@ -85,7 +85,7 @@
<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="../#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">🔑 Sign in</a>

View file

@ -93,7 +93,7 @@
<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="./#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">🔑 Sign in</a>
@ -421,7 +421,7 @@ process.exit(0);"</pre>
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script>
<script type="module" src="./js/register-flow.js?v=20260907signin"></script>
<script type="module" src="./js/register-flow.js?v=20260908tld"></script>
<script defer src="./js/site-footer.js?v=20260907rel"></script>
<script defer src="./js/profile-menu.js?v=20260907inline"></script>

View file

@ -169,6 +169,19 @@ async function client() {
function startFlow(name) {
state.name = name;
state.tld = null;
state.signInOnly = false;
open();
stepWallet();
}
// Start a TLD mint. Same wallet-choice + fund flow as a name register, but
// the mint step calls registerTldWith(Built|External)Wallet instead of the
// name variant, and the "done" screen skips the record editor since a TLD
// certificate goes to the operator's token address as-is.
function startTldFlow(label, opts = {}) {
state.name = `.${label}`; // used for headings only
state.tld = { label, serviceFeeSats: BigInt(opts.serviceFeeSats ?? 0n) };
state.signInOnly = false;
open();
stepWallet();
@ -190,14 +203,16 @@ function startSignIn(mode) {
// Public API
window.siriusRegisterName = startFlow;
window.siriusRegisterTld = startTldFlow;
window.siriusSignInWallet = startSignIn;
// ---------- step 1: wallet choice ----------
function stepWallet() {
const saved = BNS.BuiltInWallet.exists();
const kind = state.tld ? "TLD" : "name";
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
<p class="sub">First, the wallet that will <b>own</b> the ${esc(kind)}. The certificate is minted straight
to it we never hold it.</p>
${saved ? `<button class="wopt" id="w-unlock"><span class="ic">🔓</span><span>
@ -303,6 +318,10 @@ async function stepExternalConfirm(session, label) {
return;
}
// TLD external path: keep the session, hand off to the TLD confirm/mint
// steps which know to call registerTldWithExternalWallet.
if (state.tld) { state.session = session; return stepConfirmTld(); }
const 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>
@ -479,6 +498,10 @@ async function stepConfirm() {
render(`<h3>Preparing…</h3><p class="sub">Building your registration transaction.</p>`);
try {
const c = await client();
// TLD flow skips the name-availability check and the per-name quote —
// registerTldWith*Wallet builds the tx internally and checks the on-chain
// TLD registry itself. Just show a simple confirm screen with the fee.
if (state.tld) return stepConfirmTld();
const avail = await BNS.checkAvailability(c, state.name);
if (!avail.available) throw new Error(`"${avail.name}" was just registered by someone else`);
const quote = await BNS.quoteRegistration(c, {
@ -509,6 +532,73 @@ async function stepConfirm() {
}
}
// ---------- TLD confirm ----------
async function stepConfirmTld() {
const feeSats = state.tld.serviceFeeSats;
const owner = state.wallet ? state.wallet.address : (state.session ? (await state.session.getAddresses())[0] : "your address");
render(`
<h3>Confirm <b>${esc(state.name)}</b></h3>
<p class="sub">One transaction. It mints the TLD certificate to <b>your</b> token address; you become
the registry operator for every second-level name under it.</p>
<table class="price">
<tr><td>Service fee${feeSats === 0n ? " <small>(none on chipnet)</small>" : ""}</td>
<td>${sats(feeSats)} sat</td></tr>
<tr><td>Beacon dust + chain fee <small>(covered by wallet)</small></td>
<td>~1,300 sat</td></tr>
</table>
<div class="note" style="margin-top:16px">Minted to <span class="mono">${esc(owner)}</span>.
That wallet becomes the operator for this TLD.</div>
<div class="row end"><button class="btn ghost" id="back">Back</button>
<button class="btn acid" id="go">Register sign &amp; broadcast</button></div>
`);
$("back").onclick = stepFund;
$("go").onclick = stepRegisterTld;
}
async function stepRegisterTld() {
render(`<h3>Registering ${esc(state.name)}</h3>
<p class="sub">Signing in your browser and broadcasting to the chain. Do not close this tab.</p>
<div class="steps-log" id="log"></div>`);
const log = $("log");
const say = (s) => { const d = document.createElement("div"); d.textContent = s; log.appendChild(d); log.scrollTop = log.scrollHeight; };
const serviceFee = { address: BNS.REGISTRAR?.serviceFee?.address ?? null, sats: state.tld.serviceFeeSats };
try {
const res = state.session
? await BNS.registerTldWithExternalWallet(await client(), {
session: state.session, tld: state.tld.label, records: {},
serviceFee, tldListUrl: "https://navigate.st/api/tlds", onProgress: say,
})
: await BNS.registerTldWithBuiltInWallet(await client(), {
wallet: state.wallet, tld: state.tld.label, records: {},
serviceFee, tldListUrl: "https://navigate.st/api/tlds", onProgress: say,
});
state.result = res;
stepDoneTld();
} catch (e) {
err(e);
const row = document.createElement("div"); row.className = "row end";
row.innerHTML = `<button class="btn ghost" id="back">Back</button>`;
sheet.appendChild(row);
$("back").onclick = stepConfirmTld;
}
}
function stepDoneTld() {
const r = state.result;
render(`
<h3><span class="ok"></span> <b>.${esc(r.tld)}</b> is yours</h3>
<p class="sub">The TLD certificate is on the chain, held by your key. Second-level names under
<b>.${esc(r.tld)}</b> now live in your registry.</p>
<table class="price">
<tr><td>Transaction</td><td>${esc(r.txid.slice(0,20))}</td></tr>
<tr><td>Certificate ID</td><td>${esc(r.category.slice(0,20))}</td></tr>
<tr><td>Operator</td><td>${esc(r.ownerAddress.slice(0,22))}</td></tr>
</table>
<div class="row end"><button class="btn acid" id="fin">Done</button></div>
`);
$("fin").onclick = close;
}
// ---------- register ----------
async function stepRegister() {
render(`<h3>Registering ${esc(state.name)}</h3>

View file

@ -31,7 +31,7 @@
<div>
<h5>Product</h5>
<ul>
<li><a href="${s("#search-input")}">🪪 Register a name</a></li>
<li><a href="${s("#search-input")}"> Register a name</a></li>
<li><a href="${s("tld.html")}">🌐 Register a TLD</a></li>
<li><a href="${s("portal.html")}">🔑 My names portal</a></li>
<li><a href="${s("theseus/")}">🧭 Theseus browser</a></li>

View file

@ -114,7 +114,7 @@
<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="./#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>

View file

@ -90,7 +90,7 @@
<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="../#search-input">✏️ Register</a>
<a href="../tld.html">🌐 TLD</a>
<a href="./" class="here"><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">🔑 Sign in</a>

413
tld.html
View file

@ -40,25 +40,13 @@
h1 .g{color:var(--acid)}
.tag{color:var(--mut);font-size:1rem;max-width:600px;margin:0 auto}
h2{font-size:1.25rem;margin:0 0 .4rem}
h3{font-size:1rem;margin:0 0 .35rem}
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}
.field-hint{color:var(--dim);font-size:11.5px;margin-left:8px}
textarea,input[type=text],input[type=password]{
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}
@ -70,44 +58,34 @@
.status.ok{border-left:3px solid var(--ok)}
.status.err{border-left:3px solid var(--taken);color:var(--taken)}
.status.warn{border-left:3px solid var(--warn);color:var(--warn)}
.hidden{display:none}
.err{color:var(--taken);font-size:13.5px;margin-top:10px}
.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}
/* TLD search cell */
.search{max-width:600px;margin:1.5rem auto 0}
.searchrow{display:flex;gap:8px}
.searchrow input{flex:1;padding:14px 18px;font-size:17px;border-radius:12px;border:1px solid #ffffff22;background:#141a24;color:var(--ink)}
.searchrow input:focus{border-color:#4b7bec;outline:none}
.searchrow input{flex:1;padding:14px 18px;font-size:17px;border-radius:12px;border:1px solid #ffffff22;background:#141a24;color:var(--ink);font-family:inherit;outline:none}
.searchrow input:focus{border-color:#4b7bec}
.searchhint{font-size:12.5px;color:var(--mut);margin-top:8px;text-align:center}
.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:14px 18px;margin-top:12px}
.result-card .lbl{font-family:ui-monospace,monospace;font-size:18px;color:var(--ink)}
.result-card .lbl .dot{color:var(--acid)}
/* TLD registry table */
/* Registered-TLDs table */
table.tlds{width:100%;border-collapse:collapse;margin-top:1rem;font-size:13px}
table.tlds th,table.tlds td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
table.tlds th{color:var(--mut);font-weight:500;font-size:11.5px;text-transform:uppercase;letter-spacing:.4px}
table.tlds .tld{font-family:ui-monospace,monospace;color:var(--acid);font-size:13.5px;font-weight:600}
table.tlds .cat{font-family:ui-monospace,monospace;color:var(--mut);font-size:11.5px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Registration progress */
.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:140px;overflow-y:auto;display:none}
.steps-log.on{display:block}
.steps-log div::before{content:" ";color:var(--acid)}
.msg{font-size:13px;margin-top:12px}
.msg.ok{color:var(--ok)} .msg.err{color:var(--taken)}
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
</style>
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script>
</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="./#search-input">✏️ Register</a>
<a href="./tld.html" class="here">🌐 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">🔑 Sign in</a>
@ -125,89 +103,17 @@
<section id="search-section">
<h2>Search a TLD</h2>
<p class="muted">Type a label (az, 09, hyphen; up to 16 chars). We check the on-chain TLD
registry live and, if the label is free, show a register button.</p>
<p class="muted">Type a label (az, 09, hyphen; up to 16 chars). Available TLDs get a Register
button that opens the shared wallet-choice modal — the same flow name registration uses.</p>
<div class="search">
<div class="searchrow">
<input id="q" placeholder="your-tld" spellcheck="false" autocomplete="off" autofocus maxlength="16">
</div>
<div class="searchhint">Available TLDs appear with a register button. Taken labels show which category holds them.</div>
<div class="searchhint">Register minting to your token address; you become the registry operator.</div>
</div>
<div id="result"></div>
</section>
<section id="signin-section" class="hidden">
<h2>Sign in to mint</h2>
<p class="muted">The TLD certificate lands in the token address of the wallet that signs the mint —
that wallet becomes the registry operator.</p>
<div class="authbox" style="margin-top:14px">
<div class="card" id="card-new">
<h3>🆕 New wallet</h3>
<p>Generate a fresh 12-word phrase in this tab. Nothing is saved — write it down before continuing.</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></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. There is no reset if I lose it.</span>
</label>
<div class="row">
<button class="btn acid" id="new-signin-btn" disabled>Continue →</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 phrase. It stays in memory only.</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">Continue →</button>
</div>
<div id="signin-err" class="err hidden"></div>
</div>
<div class="card" id="card-wc">
<h3>🔗 WizardConnect</h3>
<p>Cashonize 0.9+ or Paytaca. The wallet signs the mint on your device.</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>Paste this into your WC-capable wallet:</label>
<div style="display:flex;gap:8px;align-items:center;margin-top:6px">
<code id="wc-uri" style="flex:1;font-size:12px"></code>
<button class="btn ghost small" 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 small" id="wc-cancel" type="button">Cancel</button></div>
</div>
<div id="wc-err" class="err hidden"></div>
</div>
</div>
</section>
<section id="mint-section" class="hidden">
<h2>Mint <span id="mint-label" class="mono" style="color:var(--acid)"></span></h2>
<div class="card">
<div class="status" id="mint-wallet">Signed in as <code id="mint-addr"></code></div>
<label>Service fee (USD) <span class="field-hint">chipnet placeholder rate; mainnet will use an oracle</span></label>
<input type="text" id="mint-usd" value="5" style="max-width:200px">
<div class="status" id="mint-price">= <b>0</b> sats · rate <b>0</b> sats/USD</div>
<div class="row">
<button class="btn acid" id="mint-submit" disabled>Register</button>
<button class="btn ghost small" id="mint-back">← Back to search</button>
<button class="btn ghost small" id="mint-signout">Sign out</button>
</div>
<div class="steps-log" id="mint-log"></div>
<div id="mint-msg" class="msg"></div>
</div>
</section>
<section id="listing">
<h2>Registered TLDs</h2>
<p class="muted">Fetched from the on-chain TLD registry via the public gateway. Refresh the page for the latest.</p>
@ -222,30 +128,19 @@
<footer id="site-footer"></footer>
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script>
<script type="module">
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260907tldext";
<script>
// Simple oracle stub: static chipnet rate for testing. Same number the
// portal uses. Mainnet swaps for a real price feed.
const CHIPNET_SATS_PER_USD = 250_000;
const DEFAULT_USD = 5;
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
const validLabel = (s) => /^[a-z0-9-]{1,16}$/.test(s);
const normLabel = (s) => String(s).toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 16);
// ---------- oracle stub ----------
// Same chipnet-only static rate as the portal. Mainnet swaps this for a real
// price feed; the sats output is what actually gets locked into the mint tx.
const CHIPNET_SATS_PER_USD = 250_000; // ~$400/BCH test rate
const usdToSats = (u) => {
const n = Number(String(u).replace(/[^\d.]/g, "")) || 0;
return Math.max(0, Math.round(n * CHIPNET_SATS_PER_USD));
};
// ---------- state ----------
let wallet = null; // built-in wallet or WC wrapper
let candidateLabel = null; // TLD label the user chose to register
// ---------- TLD list (live) ----------
// ---------- registered-TLD list ----------
let registeredTlds = null;
async function loadTldList() {
const status = $("tlds-status"), tbody = $("tlds-table").querySelector("tbody");
try {
@ -260,41 +155,33 @@ async function loadTldList() {
$("tlds-table").style.display = "";
status.className = "status ok";
status.textContent = `${tlds.length} TLDs registered on chipnet.`;
return tlds.map((t) => t.tld);
registeredTlds = tlds.map((t) => t.tld);
refreshSearch();
} catch (e) {
status.className = "status err";
status.textContent = "Could not load TLD list: " + (e.message || e);
return null;
}
}
let registeredTlds = null;
loadTldList().then((list) => { registeredTlds = list; refreshSearch(); });
// ---------- search + result ----------
function validLabel(s) { return /^[a-z0-9-]{1,16}$/.test(s); }
function normLabel(s) { return String(s).toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 16); }
loadTldList();
// ---------- search result renderer ----------
function renderResult(label, state) {
const res = $("result");
if (!label) { res.innerHTML = ""; return; }
if (state === "checking") {
res.innerHTML = `<div class="result-card">
<div class="lbl"><span class="dot">.</span>${esc(label)}</div>
<span class="badge b-check">checking…</span>
<span></span>
</div>`;
<span class="badge b-check">checking…</span><span></span></div>`;
return;
}
if (state === "taken") {
res.innerHTML = `<div class="result-card">
<div class="lbl"><span class="dot">.</span>${esc(label)}</div>
<span class="badge b-taken">taken</span>
<span></span>
</div>
<p class="dim" style="margin-top:8px">Someone already minted <code>.${esc(label)}</code>. Try a different label — TLDs are first-come, first-served.</p>`;
<span class="badge b-taken">taken</span><span></span></div>
<p class="dim" style="margin-top:8px">Someone already minted <code>.${esc(label)}</code>. Try a
different label — TLDs are first-come, first-served.</p>`;
return;
}
// available
res.innerHTML = `<div class="result-card">
<div class="lbl"><span class="dot">.</span>${esc(label)}</div>
<span class="badge b-ok">available</span>
@ -303,20 +190,19 @@ function renderResult(label, state) {
$("go-register").onclick = () => beginRegister(label);
}
// ---------- search wiring ----------
let currentLabel = "";
function refreshSearch() {
const raw = normLabel($("q").value);
currentLabel = raw;
if (!raw) { renderResult("", null); return; }
if (!validLabel(raw)) { renderResult(raw, "invalid"); return; }
// If we already have the registry snapshot, decide locally — no API round-trip.
if (registeredTlds !== null) {
renderResult(raw, registeredTlds.includes(raw) ? "taken" : "ok");
return;
}
renderResult(raw, "checking");
}
$("q").addEventListener("input", () => {
$("q").value = normLabel($("q").value);
refreshSearch();
@ -327,238 +213,33 @@ $("q").addEventListener("keydown", (e) => {
}
});
// ---------- flow: search → wallet → mint ----------
// ---------- mint delegation ----------
// Register button hands the label off to the shared modal (register-flow.js
// — window.siriusRegisterTld). That modal owns the wallet-choice, fund,
// confirm, and mint steps — same UX as name registration.
function beginRegister(label) {
candidateLabel = label;
$("mint-label").textContent = "." + label;
// Show the sign-in section if not signed in; otherwise straight to mint.
if (!wallet) {
$("signin-section").classList.remove("hidden");
$("signin-section").scrollIntoView({ behavior: "smooth", block: "start" });
} else {
showMintSection();
}
}
function showMintSection() {
$("signin-section").classList.add("hidden");
$("mint-section").classList.remove("hidden");
$("mint-addr").textContent = wallet.address;
updateMintPrice();
updateMintButton();
$("mint-section").scrollIntoView({ behavior: "smooth", block: "start" });
}
function backToSearch() {
$("mint-section").classList.add("hidden");
$("search-section").scrollIntoView({ behavior: "smooth", block: "start" });
}
$("mint-back").addEventListener("click", backToSearch);
// ---------- wallet: import ----------
$("signin-btn").addEventListener("click", async () => {
const raw = $("seed").value.trim();
const errBox = $("signin-err");
errBox.classList.add("hidden");
if (!raw) { errBox.textContent = "Paste your seed phrase to continue."; errBox.classList.remove("hidden"); return; }
const btn = $("signin-btn");
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 = "Continue →";
return;
}
$("seed").value = "";
btn.disabled = false; btn.textContent = "Continue →";
writeProfile(wallet);
showMintSection();
const call = () => window.siriusRegisterTld(label, {
serviceFeeSats: BigInt(DEFAULT_USD * CHIPNET_SATS_PER_USD),
});
// ---------- wallet: new ----------
$("new-btn").addEventListener("click", async () => {
const btn = $("new-btn"), errBox = $("new-err");
errBox.classList.add("hidden");
btn.disabled = true; btn.textContent = "Generating…";
try {
const fresh = await BNS.BuiltInWallet.create();
wallet = fresh;
$("new-seed").value = fresh.mnemonic;
$("new-phrase").classList.remove("hidden");
btn.textContent = "Phrase generated";
} catch (e) {
errBox.textContent = "Could not generate: " + (e.message || e);
errBox.classList.remove("hidden");
btn.disabled = false; btn.textContent = "Generate a phrase";
if (typeof window.siriusRegisterTld === "function") { call(); return; }
// Module still loading — retry until it lands.
const btn = $("go-register");
const orig = btn?.textContent;
if (btn) { btn.disabled = true; btn.textContent = "loading…"; }
const t = setInterval(() => {
if (typeof window.siriusRegisterTld === "function") {
clearInterval(t);
if (btn) { btn.disabled = false; btn.textContent = orig; }
call();
}
});
$("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", () => {
if (!wallet) return;
$("new-seed").value = "";
$("new-phrase").classList.add("hidden");
writeProfile(wallet);
showMintSection();
});
// ---------- wallet: WC ----------
$("wc-btn").addEventListener("click", async () => {
const btn = $("wc-btn"), errBox = $("wc-err"), 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,
crypto: {
pubkeyToAddress: BNS.pubkeyToAddress,
pubkeyToLockingBytecode: BNS.pubkeyToLockingBytecode,
binToHex: BNS.binToHex,
txidOfHex: BNS.txidOfHex,
},
});
} catch (e) {
errBox.textContent = "Could not start WC: " + (e.message || e);
errBox.classList.remove("hidden");
btn.disabled = false; btn.textContent = "Connect wallet";
return;
}
$("wc-uri").textContent = session.qrUri ?? session.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;
}
const addrs = await session.getAddresses();
if (!addrs?.length) {
errBox.textContent = "Wallet returned no addresses.";
errBox.classList.remove("hidden");
uriBox.classList.add("hidden");
btn.disabled = false; btn.textContent = "Connect wallet";
try { await session.disconnect(); } catch {}
return;
}
wallet = {
source: "wc",
address: addrs[0],
tokenAddress: BNS.toTokenAddress(addrs[0]),
watchedAddresses: addrs,
session,
};
writeProfile(wallet);
uriBox.classList.add("hidden");
btn.disabled = false; btn.textContent = "Connect wallet";
showMintSection();
});
$("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));
// ---------- profile persistence (mirrors portal.html) ----------
function writeProfile(w) {
try {
localStorage.setItem("siriusProfile", JSON.stringify({
address: w.address, tokenAddress: w.tokenAddress,
source: w.source ?? "seed", signedInAt: Date.now(),
}));
} catch {}
window.dispatchEvent(new Event("siriusProfileChanged"));
}
function clearProfile() {
try { localStorage.removeItem("siriusProfile"); } catch {}
window.dispatchEvent(new Event("siriusProfileChanged"));
}, 100);
}
$("mint-signout").addEventListener("click", async () => {
if (wallet?.source === "wc" && wallet.session) {
try { await wallet.session.disconnect(); } catch {}
}
wallet = null;
clearProfile();
$("mint-section").classList.add("hidden");
$("signin-section").classList.remove("hidden");
});
// ---------- mint ----------
function updateMintPrice() {
const usd = $("mint-usd").value.trim() || "0";
const sats = usdToSats(usd);
$("mint-price").innerHTML = `= <b>${sats.toLocaleString()}</b> sats · rate <b>${CHIPNET_SATS_PER_USD.toLocaleString()}</b> sats/USD (chipnet)`;
$("mint-submit").textContent = `Register .${candidateLabel} (~${sats.toLocaleString()} sats)`;
}
function updateMintButton() {
$("mint-submit").disabled = !wallet || !candidateLabel;
}
$("mint-usd").addEventListener("input", updateMintPrice);
$("mint-submit").addEventListener("click", async () => {
if (!wallet || !candidateLabel) return;
const sats = BigInt(usdToSats($("mint-usd").value.trim() || "0"));
const msg = $("mint-msg"); msg.textContent = ""; msg.className = "msg";
const log = $("mint-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; };
$("mint-submit").disabled = true;
$("mint-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: candidateLabel, records: {},
serviceFee, tldListUrl: "https://navigate.st/api/tlds",
onProgress: push,
})
: await BNS.registerTldWithBuiltInWallet(el, {
wallet, tld: candidateLabel, records: {},
serviceFee, tldListUrl: "https://navigate.st/api/tlds",
onProgress: push,
});
push("broadcast: " + (res.txid || "(no txid)"));
msg.className = "msg ok";
msg.innerHTML = `✅ <b>.${esc(candidateLabel)}</b> minted. Category <code>${esc((res.category || "").slice(0, 16))}…</code><br>
The certificate now lives in your token address; names can be registered under it from
<a href="./#search-input">the name search</a>.`;
$("mint-submit").textContent = "Registered ✓";
// Refresh the live TLD table so the new one shows up.
setTimeout(() => loadTldList().then((l) => { registeredTlds = l; refreshSearch(); }), 3500);
} catch (e) {
msg.className = "msg err";
const raw = String(e.message || e);
if (/wallet is empty/i.test(raw)) {
msg.innerHTML = `Failed: wallet is empty. Fund <code>${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);
$("mint-submit").disabled = false;
updateMintPrice();
} finally {
try { el?.close?.(); } catch {}
}
});
// ---------- ?q= deep-link (from landing search too, if someone comes with a label) ----------
// ---------- ?q= deep-link (from landing links etc.) ----------
(() => {
const q = normLabel(new URLSearchParams(location.search).get("q") || "");
if (!q) return;
$("q").value = q;
// wait for registeredTlds to load
const wait = setInterval(() => {
if (registeredTlds === null) return;
clearInterval(wait);
@ -567,6 +248,8 @@ $("mint-submit").addEventListener("click", async () => {
}, 150);
})();
</script>
<script type="module" src="./js/register-flow.js?v=20260908tld"></script>
<script defer src="./js/site-footer.js?v=20260907rel"></script>
<script defer src="./js/profile-menu.js?v=20260907inline"></script>
</body>