Every second-level name registration under a TLD now routes 90% of the
service fee to whoever holds that TLD's certificate on chain, with 10%
going to the platform address. That's the economic incentive for
minting a TLD: you earn from every name registered under it.
The mechanism, end to end:
1. resolver-web.js fetchTldMap now also records mintScriptHex — the
scriptPubKey of the TREG output that carries each TLD's NFT.
Exported so registrar can decode it into a cashaddr with libauth.
MVP: this is the ORIGINAL owner; NFT transfers after mint are not
traced yet (a follow-up will walk the chain of transfers).
2. registrar.js gains findTldOwnerAddress(client, tld) and
splitServiceFee(sats). The split constants live at the top of the
file (TLD_OWNER_SHARE_NUM/DEN = 90/100) so the ratio moves in one
place. Rounding: BigInt division favours the platform on odd sat
counts so the two shares always sum EXACTLY to the input.
3. quoteRegistration wraps the existing flow: it derives the TLD from
the name, looks up the TLD owner, and if the owner ≠ buyer it asks
buildRegistrationTx to add a second fee output. If the owner
couldn't be resolved (TLD not registered, decode failure) the full
fee stays on the platform address — the buyer still pays the same
amount either way.
4. register-tx.js buildRegistrationTx accepts tldFeeAddress/tldFeeSats
and, when set, emits an extra P2PKH output for the TLD owner. Sits
between the beacon dust and the platform-fee output; outputMap
records .tldOwnerFee so callers can find it. costs also carries
tldOwnerFeeSats and netCostSats includes it.
5. priceSummary in registrar-config splits the 'Service fee' row into
'Service fee — TLD owner (90%)' + 'Service fee — platform (10%)'
whenever tldOwnerFeeSats > 0, with a per-line note explaining
where the money goes.
Bundle: re-exported findTldOwnerAddress + splitServiceFee from
register-entry.js. Rebuilt bns-register.js (~34 kB) and deployed;
cache-buster bumped to ?v=20260908split on portal / admin /
register-flow.js.
Verified live: findTldOwnerAddress('.bch') returns the operator
cashaddr; quoteRegistration('tester42.bch') builds cleanly with a
9,000/1,000 split output pair on a 10,000-sat fee; priceSummary
renders both lines. No regressions on the TLD-mint flow (buyer IS
the TLD owner there — split short-circuits and it stays a single
fee output as before).
790 lines
35 KiB
HTML
790 lines
35 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Portal — your names on Sirius.X</title>
|
||
<meta name="description" content="Sign in with your Bitcoin Cash seed phrase to see every BCNR name your wallet holds and manage records. Your keys never leave the browser.">
|
||
<link rel="icon" href="./assets/favicon.svg" type="image/svg+xml">
|
||
<link rel="mask-icon" href="./assets/favicon.svg" color="#d6ff3d">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:site_name" content="Sirius.X">
|
||
<meta property="og:title" content="Sirius.X portal — your BCNR names">
|
||
<meta property="og:description" content="Sign in with your seed phrase to see every name your wallet holds and manage records. Keys never leave the browser.">
|
||
<meta property="og:url" content="https://silentmode.st/sirius-x/portal.html">
|
||
<meta property="og:image" content="https://silentmode.st/sirius-x/brand/banner.svg">
|
||
<meta name="twitter:card" content="summary">
|
||
<style>
|
||
:root{
|
||
--bg:#0b0e14; --panel:#141a24; --panel2:#18202c; --line:rgba(255,255,255,.09);
|
||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
||
--ok:#4fd1a5; --taken:#f6768a; --warn:#ffc75f;
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:radial-gradient(1100px 560px at 50% -12%,#16202e,var(--bg));
|
||
color:var(--ink);font:16px/1.65 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh}
|
||
a{color:var(--acid)}
|
||
.wrap{max-width:820px;margin:0 auto;padding:0 1.2rem}
|
||
.topnav{position:sticky;top:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap;
|
||
padding:10px 1.2rem;background:rgba(11,14,20,.82);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
|
||
.topnav .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:14px;font-size:15px;letter-spacing:.2px}
|
||
.topnav .brand .g{color:var(--acid)}
|
||
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px}
|
||
.topnav a:hover{color:var(--ink);background:var(--panel)}
|
||
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)}
|
||
.topnav a.portal{margin-left:auto;background:rgba(214,255,61,.10);color:var(--acid);font-weight:500}
|
||
.topnav a.portal:hover{background:rgba(214,255,61,.18)}
|
||
header.hero{text-align:center;padding:3rem 1.2rem 1rem}
|
||
.mark{font-size:44px;line-height:1}
|
||
h1{font-size:clamp(1.6rem,4.5vw,2.2rem);margin:.3rem 0 .3rem}
|
||
h1 .g{color:var(--acid)}
|
||
.tag{color:var(--mut);font-size:1rem;max-width:580px;margin:0 auto}
|
||
h2{font-size:1.25rem;margin:0 0 .4rem}
|
||
section{padding:2rem 0;border-top:1px solid var(--line);margin-top:2rem}
|
||
section:first-of-type{border-top:none;margin-top:1rem}
|
||
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin-top:1rem}
|
||
label{display:block;font-size:13px;color:var(--mut);margin:14px 0 5px}
|
||
textarea,input[type=password],input[type=text]{
|
||
width:100%;padding:11px 13px;border-radius:10px;border:1px solid #ffffff22;background:#0e131b;
|
||
color:var(--ink);font-size:14px;font-family:inherit;outline:none
|
||
}
|
||
textarea{min-height:74px;resize:vertical;font-family:ui-monospace,monospace;font-size:13px}
|
||
textarea:focus,input:focus{border-color:#4b7bec}
|
||
.btn{display:inline-block;text-decoration:none;padding:10px 18px;border-radius:10px;background:#4b7bec;color:#fff;font-size:14px;border:none;cursor:pointer;font-family:inherit}
|
||
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
|
||
.btn.acid{background:var(--acid);color:#0b0e14;font-weight:600}
|
||
.btn.small{padding:6px 12px;font-size:12.5px}
|
||
.btn:hover{filter:brightness(1.12)}
|
||
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
|
||
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px;align-items:center}
|
||
.muted{color:var(--mut);font-size:13px}
|
||
.dim{color:var(--dim);font-size:12.5px}
|
||
code,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px;word-break:break-all}
|
||
.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:999px;font-weight:600}
|
||
.b-x{background:rgba(214,255,61,.14);color:var(--acid)}
|
||
.b-warn{background:rgba(255,199,95,.14);color:var(--warn)}
|
||
.err{color:var(--taken);font-size:13.5px;margin-top:10px}
|
||
.status{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 14px;font-size:13px;color:var(--mut);margin-top:12px}
|
||
.status.ok{border-left:3px solid var(--ok)}
|
||
.status.err{border-left:3px solid var(--taken);color:var(--taken)}
|
||
.hidden{display:none}
|
||
.authbox{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
|
||
.authbox .card{margin:0}
|
||
.authbox h3{margin:0 0 .3rem;font-size:1rem}
|
||
.authbox p{margin:0;color:var(--mut);font-size:13px}
|
||
.authbox .card > .cta{margin-top:14px}
|
||
.name-row{display:grid;grid-template-columns:1fr auto auto auto;gap:10px;align-items:center;
|
||
padding:14px 16px;background:var(--panel2);border:1px solid var(--line);border-radius:12px;margin-top:8px}
|
||
.name-row .n{font-family:ui-monospace,monospace;font-size:15px}
|
||
.name-row .rec{color:var(--mut);font-family:ui-monospace,monospace;font-size:12px;grid-column:1/-1;margin-top:6px;word-break:break-all}
|
||
.name-row .rec .k{color:var(--dim)}
|
||
/* record editor modal */
|
||
.modal{position:fixed;inset:0;background:rgba(6,9,14,.82);backdrop-filter:blur(3px);
|
||
display:none;align-items:flex-start;justify-content:center;padding:3vh 1rem;overflow-y:auto;z-index:50}
|
||
.modal.open{display:flex}
|
||
.sheet{background:var(--panel);border:1px solid var(--line);border-radius:16px;max-width:620px;width:100%;
|
||
padding:24px 26px;position:relative}
|
||
.sheet h3{margin:0 0 .3rem;font-size:1.15rem;font-family:ui-monospace,monospace}
|
||
.sheet .sub{color:var(--mut);font-size:13px;margin:0 0 1.2rem}
|
||
.sheet .x{position:absolute;top:14px;right:16px;background:none;border:none;color:var(--dim);font-size:22px;cursor:pointer;line-height:1}
|
||
.sheet label{display:block;font-size:12px;color:var(--mut);margin:12px 0 4px;text-transform:uppercase;letter-spacing:.4px}
|
||
.sheet .field-hint{color:var(--dim);font-size:11.5px;margin-left:8px;text-transform:none;letter-spacing:0}
|
||
.sheet input[type=text],.sheet textarea{
|
||
width:100%;padding:9px 12px;border-radius:8px;border:1px solid #ffffff22;background:#0e131b;
|
||
color:var(--ink);font-size:13px;font-family:ui-monospace,monospace;outline:none
|
||
}
|
||
.sheet textarea{min-height:60px;resize:vertical}
|
||
.sheet input:focus,.sheet textarea:focus{border-color:#4b7bec}
|
||
.sheet .budget{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--mut);margin-top:14px;padding:8px 12px;background:var(--panel2);border-radius:8px}
|
||
.sheet .budget.over{background:rgba(246,118,138,.15);color:var(--taken)}
|
||
.sheet .actions{display:flex;gap:10px;justify-content:flex-end;margin-top:16px}
|
||
.sheet .steps-log{margin-top:12px;font-family:ui-monospace,monospace;font-size:12px;color:var(--mut);background:#0e131b;border:1px solid var(--line);border-radius:8px;padding:10px 12px;max-height:120px;overflow-y:auto;display:none}
|
||
.sheet .steps-log.on{display:block}
|
||
.sheet .steps-log div::before{content:"› ";color:var(--acid)}
|
||
.sheet .msg{font-size:13px;margin-top:12px}
|
||
.sheet .msg.err{color:var(--taken)} .sheet .msg.ok{color:var(--ok)}
|
||
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
|
||
@media (max-width:520px){
|
||
.name-row{grid-template-columns:1fr auto}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="topnav">
|
||
<a class="brand" href="./">⭐ <span class="g">Sirius.X</span></a>
|
||
<a href="./#try">Try it</a>
|
||
<a href="./#search-input">✏️ Register</a>
|
||
<a href="./tld.html">🌐 TLD</a>
|
||
<a href="./theseus/"><svg width="14" height="14" viewBox="0 0 64 64" style="vertical-align:-2px;margin-right:4px" aria-hidden="true"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/><path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/><circle cx="32" cy="32" r="3" fill="#0b0e14"/></svg>Theseus</a>
|
||
<a href="./portal.html" class="portal here">🔑 Sign in</a>
|
||
</nav>
|
||
|
||
<header class="hero">
|
||
<div class="mark">🔑</div>
|
||
<h1>Your <span class="g">names</span> portal</h1>
|
||
<p class="tag">Sign in with your Bitcoin Cash seed phrase or connect an external wallet to see
|
||
every BCNR name your wallet holds. Your keys never leave this browser tab.</p>
|
||
</header>
|
||
|
||
<div class="wrap">
|
||
|
||
<section id="signin">
|
||
<h2>Sign in</h2>
|
||
<p class="muted">The same wallet that will hold your names does the signing. Pick how to sign in —
|
||
the tabbed modal has all four options.</p>
|
||
<div class="card" style="text-align:center;padding:32px 22px">
|
||
<button class="btn acid" id="open-signin" style="padding:14px 28px;font-size:15px">🔑 Sign in or create wallet</button>
|
||
<p class="dim" style="margin-top:14px;font-size:12.5px">Opens the shared modal with tabs:
|
||
<b>🔓 Unlock · 🆕 Create · 📥 Import · 🔗 WizardConnect</b>.
|
||
Same UI used everywhere on Sirius.X.</p>
|
||
</div>
|
||
<div id="signin-err" class="err hidden"></div>
|
||
</section>
|
||
|
||
<section id="me" class="hidden">
|
||
<h2>Signed in</h2>
|
||
|
||
<div class="card" id="tld-register-card">
|
||
<h3 style="margin:0 0 .3rem;font-size:1.05rem">Register a new TLD</h3>
|
||
<p class="muted" style="margin:0 0 .8rem">TLDs are per-registry NFTs on the chipnet TLD beacon.
|
||
Public mints go through a service-fee output to fund the registry. Chipnet uses a placeholder
|
||
rate; mainnet will price by label length via a real oracle.</p>
|
||
<label>Label <span class="dim" style="margin-left:6px">1–16 chars, [a-z0-9-]</span></label>
|
||
<input type="text" id="tld-label" placeholder="myregistry" autocomplete="off" spellcheck="false" maxlength="16">
|
||
<label style="margin-top:12px">Service fee (USD) <span class="dim" style="margin-left:6px">chipnet test rate</span></label>
|
||
<input type="text" id="tld-usd" value="5">
|
||
<div class="status" id="tld-price">= <b>0</b> sats · rate <b>0</b> sats/USD</div>
|
||
<div class="row">
|
||
<button class="btn acid small" id="tld-check" disabled>Check availability</button>
|
||
<button class="btn small" id="tld-submit" disabled>Register (~0 sats)</button>
|
||
</div>
|
||
<div id="tld-log" class="status" style="display:none;font-family:ui-monospace,monospace;font-size:12.5px;margin-top:12px"></div>
|
||
<div id="tld-msg" style="margin-top:10px;font-size:13.5px"></div>
|
||
</div>
|
||
|
||
<h2 style="margin-top:2rem">Wallet</h2>
|
||
<div class="card">
|
||
<label>Receive address</label>
|
||
<div><code id="me-addr">—</code></div>
|
||
<label style="margin-top:14px">Token address (holds your name certificates)</label>
|
||
<div><code id="me-taddr">—</code></div>
|
||
<div class="row">
|
||
<button class="btn ghost small" id="signout-btn">Sign out</button>
|
||
<span class="dim" id="me-info">—</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2 style="margin-top:2rem">Names you hold</h2>
|
||
<div class="status" id="names-status">Loading names from the chain…</div>
|
||
<div id="names-list"></div>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<!-- record-editor modal — populated by JS when a name's Edit button fires -->
|
||
<div class="modal" id="editor">
|
||
<div class="sheet">
|
||
<button class="x" id="ed-close" title="close">×</button>
|
||
<h3 id="ed-name">—</h3>
|
||
<p class="sub">Edit records. Only your key can sign this UPD; the transaction fee is a few
|
||
cents in chain fees plus the beacon dust. Leave a field blank to omit that record.</p>
|
||
|
||
<label>h <span class="field-hint">inline HTML — served as-is at the site root</span></label>
|
||
<textarea id="ed-h" placeholder="<h1>My site</h1>"></textarea>
|
||
|
||
<label>s3 <span class="field-hint">Sia bucket key, trailing slash for directories</span></label>
|
||
<input type="text" id="ed-s3" placeholder="bns/myname/">
|
||
|
||
<label>ip <span class="field-hint">IPv4 that serves this name over Host header</span></label>
|
||
<input type="text" id="ed-ip" placeholder="1.2.3.4">
|
||
|
||
<label>tls <span class="field-hint">SHA-256 of your leaf cert — chain-pinned TLS trust</span></label>
|
||
<input type="text" id="ed-tls" placeholder="hex — 64 chars, no separators">
|
||
|
||
<label>u <span class="field-hint">redirect target</span></label>
|
||
<input type="text" id="ed-u" placeholder="https://…">
|
||
|
||
<div class="budget" id="ed-budget">payload: <b><span id="ed-budget-used">0</span></b> / <span id="ed-budget-max">?</span> bytes</div>
|
||
|
||
<div class="steps-log" id="ed-log"></div>
|
||
<div class="msg" id="ed-msg"></div>
|
||
|
||
<div class="actions">
|
||
<button class="btn ghost" id="ed-cancel">Cancel</button>
|
||
<button class="btn acid" id="ed-save">Save records →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer id="site-footer"></footer>
|
||
|
||
<script type="importmap">
|
||
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
|
||
</script>
|
||
|
||
<script type="module">
|
||
// The registrar bundle exports everything we need: BuiltInWallet for local
|
||
// wallet unlock, resolveName/buildIndex for chain queries, getUtxosForAddresses
|
||
// for holdings. This portal is a read-first surface; record edits (UPD) will
|
||
// come next and will piggyback on the same signing path the registrar uses.
|
||
// Cache-buster bumped whenever the bundle changes. Chrome's in-memory ES
|
||
// module map ignores Cache-Control on the .js URL for the lifetime of a tab,
|
||
// so a plain "/js/bns-register.js" import can return a stale instance days
|
||
// after deploy. A query string forces the module loader to treat this as a
|
||
// new URL and fetch fresh bytes.
|
||
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260908split";
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
||
|
||
let wallet = null;
|
||
|
||
// Persist signed-in state so the profile dropdown (profile-menu.js) can flip
|
||
// from "onboarding" to "signed in" and any tab on the same origin (sirius.x)
|
||
// reflects the current session. The wallet keys themselves never leave this
|
||
// page — localStorage only holds a small address-only breadcrumb, no seed.
|
||
function writeProfile(w) {
|
||
try {
|
||
localStorage.setItem("siriusProfile", JSON.stringify({
|
||
address: w.address,
|
||
tokenAddress: w.tokenAddress,
|
||
source: w.source ?? "seed", // "seed" | "wc"
|
||
signedInAt: Date.now(),
|
||
}));
|
||
} catch {}
|
||
// profile-menu.js listens for this to re-render the nav dropdown in the
|
||
// same tab (the "storage" event only fires in *other* tabs).
|
||
window.dispatchEvent(new Event("siriusProfileChanged"));
|
||
}
|
||
function clearProfile() {
|
||
try { localStorage.removeItem("siriusProfile"); } catch {}
|
||
window.dispatchEvent(new Event("siriusProfileChanged"));
|
||
}
|
||
|
||
// ---------- sign in (via shared modal) ----------
|
||
// The old inline cards are gone; portal.html now delegates the whole wallet
|
||
// UX to the shared register-flow.js modal (window.siriusSignInWallet). This
|
||
// keeps portal in lockstep with the landing, tld page, and the wallet
|
||
// dropdown — one tabbed modal, everywhere.
|
||
//
|
||
// Signed-in state is picked up from siriusProfile + window.siriusWallet:
|
||
// - profile only (returning user with a saved wallet, no live wallet in
|
||
// memory) -> auto-open the modal at the Unlock tab
|
||
// - profile + wallet -> call enterPortal() and show the names list
|
||
// - no profile -> stay on the launcher card
|
||
async function adoptWalletFromModal() {
|
||
const profile = readProfile();
|
||
if (!profile) return false;
|
||
if (window.siriusWallet) {
|
||
wallet = window.siriusWallet;
|
||
await enterPortal();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function readProfile() {
|
||
try { return JSON.parse(localStorage.getItem("siriusProfile") || "null"); } catch { return null; }
|
||
}
|
||
async function ensureSignInModal() {
|
||
if (typeof window.siriusSignInWallet !== "function") {
|
||
// register-flow.js hasn't landed yet — wait briefly.
|
||
for (let i = 0; i < 60 && typeof window.siriusSignInWallet !== "function"; i++) {
|
||
await new Promise((r) => setTimeout(r, 50));
|
||
}
|
||
}
|
||
return typeof window.siriusSignInWallet === "function";
|
||
}
|
||
|
||
$("open-signin").addEventListener("click", async () => {
|
||
if (await ensureSignInModal()) window.siriusSignInWallet("import");
|
||
});
|
||
|
||
// Re-check on every profile change (fired by register-flow after finishSignIn
|
||
// and by profile-menu after sign-out). Same-tab and cross-tab both fire this.
|
||
window.addEventListener("siriusProfileChanged", async () => {
|
||
if (readProfile()) { await adoptWalletFromModal(); }
|
||
else { signOutUi(); }
|
||
});
|
||
window.addEventListener("storage", async (e) => {
|
||
if (e.key !== "siriusProfile") return;
|
||
if (readProfile()) { await adoptWalletFromModal(); }
|
||
else { signOutUi(); }
|
||
});
|
||
|
||
function signOutUi() {
|
||
wallet = null;
|
||
window.siriusWallet = null;
|
||
$("me").classList.add("hidden");
|
||
$("signin").classList.remove("hidden");
|
||
$("names-list").innerHTML = "";
|
||
$("names-status").textContent = "Loading names from the chain…";
|
||
}
|
||
|
||
// (old inline seed/create/wc handlers deleted — see the modal launcher above)
|
||
/* HANDLERS_REMOVED_MARKER
|
||
$("new-btn").addEventListener("click", async () => {
|
||
const btn = $("new-btn");
|
||
const errBox = $("new-err");
|
||
errBox.classList.add("hidden");
|
||
btn.disabled = true; btn.textContent = "Generating…";
|
||
let fresh;
|
||
try { fresh = await BNS.BuiltInWallet.create(); }
|
||
catch (e) {
|
||
errBox.textContent = "Could not generate a phrase: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
btn.disabled = false; btn.textContent = "Generate a phrase";
|
||
return;
|
||
}
|
||
wallet = fresh;
|
||
$("new-seed").value = fresh.mnemonic;
|
||
$("new-phrase").classList.remove("hidden");
|
||
btn.disabled = true; btn.textContent = "Phrase generated";
|
||
});
|
||
|
||
$("new-ack").addEventListener("change", (e) => {
|
||
$("new-signin-btn").disabled = !e.target.checked;
|
||
});
|
||
|
||
$("new-copy-btn").addEventListener("click", () => {
|
||
navigator.clipboard?.writeText($("new-seed").value);
|
||
});
|
||
|
||
$("new-signin-btn").addEventListener("click", async () => {
|
||
if (!wallet) return;
|
||
// Wipe the visible phrase immediately — it's in the wallet object now.
|
||
$("new-seed").value = "";
|
||
$("new-phrase").classList.add("hidden");
|
||
writeProfile(wallet);
|
||
await enterPortal();
|
||
});
|
||
|
||
// ---------- WizardConnect ----------
|
||
// Ports the WC flow from register.html into a sign-in surface. The portal
|
||
// doesn't need to mint — it only reads holdings and signs UPDs — so this is
|
||
// simpler than the register flow: open a session, show the URI/QR, wait for
|
||
// approval, wrap the returned addresses in a wallet-shaped object with a
|
||
// `source: "wc"` flag so downstream code (record editor, sign-out) can
|
||
// dispatch to the external-wallet path.
|
||
$("wc-btn").addEventListener("click", async () => {
|
||
const btn = $("wc-btn");
|
||
const errBox = $("wc-err");
|
||
const uriBox = $("wc-uri-box");
|
||
errBox.classList.add("hidden");
|
||
btn.disabled = true; btn.textContent = "Starting…";
|
||
let session;
|
||
try {
|
||
const mod = await import("https://silentmode.st/js/wizardconnect.js");
|
||
session = await mod.openWizardSession({
|
||
dappName: BNS.REGISTRAR.wizardConnect.dappName,
|
||
dappIcon: BNS.REGISTRAR.wizardConnect.dappIcon,
|
||
prefix: BNS.REGISTRAR.addressPrefix,
|
||
// libauth is only bundled once (in the main bundle); wizardconnect.js
|
||
// needs these pubkey/hash helpers passed through.
|
||
crypto: {
|
||
pubkeyToAddress: BNS.pubkeyToAddress,
|
||
pubkeyToLockingBytecode: BNS.pubkeyToLockingBytecode,
|
||
binToHex: BNS.binToHex,
|
||
txidOfHex: BNS.txidOfHex,
|
||
},
|
||
});
|
||
} catch (e) {
|
||
errBox.textContent = "Could not start WizardConnect: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
return;
|
||
}
|
||
const uri = session.qrUri ?? session.uri;
|
||
$("wc-uri").textContent = uri;
|
||
uriBox.classList.remove("hidden");
|
||
btn.textContent = "Waiting for wallet…";
|
||
|
||
try {
|
||
await session.ready;
|
||
} catch (e) {
|
||
errBox.textContent = "Wallet did not approve: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
uriBox.classList.add("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
try { await session.disconnect(); } catch {}
|
||
return;
|
||
}
|
||
|
||
let addresses;
|
||
try { addresses = await session.getAddresses(); }
|
||
catch (e) {
|
||
errBox.textContent = "Wallet returned no addresses: " + (e.message || e);
|
||
errBox.classList.remove("hidden");
|
||
uriBox.classList.add("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
try { await session.disconnect(); } catch {}
|
||
return;
|
||
}
|
||
if (!addresses?.length) {
|
||
errBox.textContent = "The connected wallet exposed no addresses.";
|
||
errBox.classList.remove("hidden");
|
||
uriBox.classList.add("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
try { await session.disconnect(); } catch {}
|
||
return;
|
||
}
|
||
|
||
// Wallet-shaped wrapper — enough for holdings lookup and dispatching the
|
||
// WC path in the record editor. External wallets don't expose keyFor(),
|
||
// so any codepath that needs it (TLD mint via built-in path) is guarded
|
||
// by `wallet.source !== "wc"`.
|
||
wallet = {
|
||
source: "wc",
|
||
address: addresses[0],
|
||
tokenAddress: BNS.toTokenAddress(addresses[0]),
|
||
watchedAddresses: addresses,
|
||
session,
|
||
};
|
||
writeProfile(wallet);
|
||
uriBox.classList.add("hidden");
|
||
btn.disabled = false; btn.textContent = "Connect wallet";
|
||
await enterPortal();
|
||
});
|
||
|
||
$("wc-cancel").addEventListener("click", async () => {
|
||
if (wallet?.session) { try { await wallet.session.disconnect(); } catch {} }
|
||
$("wc-uri-box").classList.add("hidden");
|
||
$("wc-btn").disabled = false;
|
||
$("wc-btn").textContent = "Connect wallet";
|
||
});
|
||
|
||
$("wc-copy").addEventListener("click", () => {
|
||
navigator.clipboard?.writeText($("wc-uri").textContent);
|
||
});
|
||
|
||
HANDLERS_REMOVED_MARKER */
|
||
|
||
// Signed-in section's own sign-out button. Delegates the profile+wallet
|
||
// teardown to the shared modal's signOutUi() (already declared above).
|
||
$("signout-btn")?.addEventListener("click", async () => {
|
||
if (wallet?.source === "wc" && wallet.session) {
|
||
try { await wallet.session.disconnect(); } catch {}
|
||
}
|
||
clearProfile();
|
||
signOutUi();
|
||
});
|
||
|
||
// ---------- ?mode= deep-link ----------
|
||
// Anyone arriving at portal.html?mode=new|import|wc|unlock opens the shared
|
||
// modal at that tab. Same handler as the wallet dropdown uses — no old
|
||
// inline forms hijack the URL.
|
||
(async function handleMode() {
|
||
const mode = new URLSearchParams(location.search).get("mode");
|
||
if (!mode) return;
|
||
if (await ensureSignInModal()) window.siriusSignInWallet(mode);
|
||
})();
|
||
|
||
// On load: if user already has a siriusProfile, adopt whatever in-memory
|
||
// wallet exists (e.g. they signed in via the modal on a nearby page and
|
||
// navigated here). If profile exists but no wallet, prompt Unlock.
|
||
(async function bootstrapSession() {
|
||
const profile = readProfile();
|
||
if (!profile) return;
|
||
const adopted = await adoptWalletFromModal();
|
||
if (adopted) return;
|
||
// Profile without in-memory wallet — offer Unlock in the modal.
|
||
if (await ensureSignInModal()) {
|
||
// If a saved encrypted wallet exists, the modal defaults to Unlock;
|
||
// otherwise fall back to Import.
|
||
const hasSaved = (() => { try { return !!localStorage.getItem("bns.wallet.v1"); } catch { return false; } })();
|
||
window.siriusSignInWallet(hasSaved ? "unlock" : "import");
|
||
}
|
||
})();
|
||
|
||
async function enterPortal() {
|
||
$("signin").classList.add("hidden");
|
||
$("me").classList.remove("hidden");
|
||
$("me-addr").textContent = wallet.address;
|
||
$("me-taddr").textContent = wallet.tokenAddress;
|
||
$("me-info").textContent = "watched addresses: " + wallet.watchedAddresses.length;
|
||
// Initialise the TLD-register card now that the wallet is available.
|
||
if (typeof updateTldPrice === "function") { updateTldPrice(); updateTldButtons(); }
|
||
await loadNames();
|
||
}
|
||
|
||
// ---------- load names ----------
|
||
// Approach: derive the scripthash for each watched address locally (the
|
||
// wallet code already knows how), send them in one POST to
|
||
// /api/holdings, and render whatever the gateway returns. Doing the
|
||
// wallet-holdings query server-side is what makes this work on browsers
|
||
// stuck behind firewalls that block chipnet electrum ports (50004,
|
||
// 50011, 62002) — the API is same-origin HTTPS on port 443, and the
|
||
// gateway can reach the full electrums the browser can't.
|
||
async function loadNames() {
|
||
const status = $("names-status");
|
||
const list = $("names-list");
|
||
list.innerHTML = "";
|
||
status.className = "status";
|
||
status.textContent = "Preparing your addresses…";
|
||
try {
|
||
const addresses = wallet.watchedAddresses;
|
||
const scripthashes = addresses.map((a) => BNS.addressToScripthash(a));
|
||
status.textContent = `Asking the gateway about ${addresses.length} addresses…`;
|
||
const r = await fetch("https://silentmode.st/api/holdings", {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify({ scripthashes }),
|
||
});
|
||
if (!r.ok) {
|
||
const detail = await r.text().catch(() => "");
|
||
throw new Error(`API ${r.status}: ${detail.slice(0, 200)}`);
|
||
}
|
||
const { count, names: mine } = await r.json();
|
||
if (!count) {
|
||
status.className = "status";
|
||
status.textContent = "No BCNR names found for this wallet. If you just registered, wait a minute for confirmation and reload.";
|
||
return;
|
||
}
|
||
status.className = "status ok";
|
||
status.textContent = `You own ${count} name${count === 1 ? "" : "s"}.`;
|
||
for (const e of mine) {
|
||
const div = document.createElement("div");
|
||
div.className = "name-row";
|
||
const records = e.records && Object.keys(e.records).length
|
||
? Object.entries(e.records).map(([k, v]) => `<span class="k">${esc(k)}=</span>${esc(String(v).slice(0, 80))}`).join(" · ")
|
||
: '<span class="k">(no records set)</span>';
|
||
const recJson = esc(JSON.stringify(e.records || {}));
|
||
div.innerHTML =
|
||
`<div class="n">${esc(e.name)}</div>` +
|
||
'<span class="badge b-x">on chain</span>' +
|
||
`<a class="btn small ghost" href="https://silentmode.st/bns/${encodeURIComponent(e.name)}/" target="_blank" rel="noopener">view →</a>` +
|
||
`<button class="btn small acid" data-edit="${esc(e.name)}" data-records='${recJson}'>Edit</button>` +
|
||
`<div class="rec">${records}</div>`;
|
||
list.appendChild(div);
|
||
}
|
||
// Wire Edit buttons after render.
|
||
list.querySelectorAll("[data-edit]").forEach((btn) => {
|
||
btn.onclick = () => openEditor(btn.getAttribute("data-edit"), JSON.parse(btn.getAttribute("data-records") || "{}"));
|
||
});
|
||
} catch (e) {
|
||
status.className = "status err";
|
||
status.textContent = "Could not load names: " + (e.message || e);
|
||
}
|
||
}
|
||
|
||
// ---------- record editor ----------
|
||
// Modal that composes a UPD payload from a small form and signs+broadcasts
|
||
// with the same code path register.html uses (BNS.setRecordsWithBuiltInWallet).
|
||
// External-wallet (WizardConnect) support piggybacks the same adapter and is a
|
||
// follow-up — the built-in path is the one this session's wallet flow can already
|
||
// sign with.
|
||
const EDITABLE_KEYS = ["h", "s3", "ip", "tls", "u"];
|
||
let editorName = null;
|
||
|
||
function openEditor(name, records) {
|
||
editorName = name;
|
||
$("ed-name").textContent = name;
|
||
for (const k of EDITABLE_KEYS) $("ed-" + k).value = records[k] || "";
|
||
$("ed-msg").textContent = "";
|
||
$("ed-msg").className = "msg";
|
||
$("ed-log").className = "steps-log";
|
||
$("ed-log").innerHTML = "";
|
||
$("ed-save").disabled = false;
|
||
$("ed-save").textContent = "Save records →";
|
||
updateBudget();
|
||
$("editor").classList.add("open");
|
||
}
|
||
function closeEditor() {
|
||
$("editor").classList.remove("open");
|
||
editorName = null;
|
||
}
|
||
$("ed-close").addEventListener("click", closeEditor);
|
||
$("ed-cancel").addEventListener("click", closeEditor);
|
||
$("editor").addEventListener("click", (e) => { if (e.target === $("editor")) closeEditor(); });
|
||
|
||
// Bytes budget: BNS payload is capped at 200 bytes total, of which the envelope
|
||
// (op + name) uses some. Show the user how many bytes their records take vs the
|
||
// remaining budget, and disable Save if they've gone over.
|
||
function collectRecords() {
|
||
const r = {};
|
||
for (const k of EDITABLE_KEYS) {
|
||
const v = $("ed-" + k).value.trim();
|
||
if (v) r[k] = v;
|
||
}
|
||
return r;
|
||
}
|
||
function updateBudget() {
|
||
if (!editorName) return;
|
||
try {
|
||
const max = BNS.payloadBudget(editorName, "UPD");
|
||
// The records object encodes to JSON inside the payload. Rough size:
|
||
// JSON.stringify(records).length gives a close-enough client-side estimate
|
||
// (the actual envelope is composed by the bundle at send time).
|
||
const used = new TextEncoder().encode(JSON.stringify(collectRecords())).length;
|
||
$("ed-budget-max").textContent = max;
|
||
$("ed-budget-used").textContent = used;
|
||
const over = used > max;
|
||
$("ed-budget").classList.toggle("over", over);
|
||
$("ed-save").disabled = over;
|
||
} catch { /* budget helper not available yet — ignore */ }
|
||
}
|
||
for (const k of EDITABLE_KEYS) $("ed-" + k).addEventListener("input", updateBudget);
|
||
|
||
$("ed-save").addEventListener("click", async () => {
|
||
if (!editorName || !wallet) return;
|
||
const records = collectRecords();
|
||
const msg = $("ed-msg");
|
||
const log = $("ed-log");
|
||
log.className = "steps-log on";
|
||
log.innerHTML = "";
|
||
const push = (t) => { const d = document.createElement("div"); d.textContent = t; log.appendChild(d); log.scrollTop = log.scrollHeight; };
|
||
msg.textContent = ""; msg.className = "msg";
|
||
$("ed-save").disabled = true;
|
||
$("ed-save").textContent = "Signing…";
|
||
let el = null;
|
||
try {
|
||
el = await BNS.connect();
|
||
push("connected to chipnet");
|
||
const res = wallet.source === "wc"
|
||
? await BNS.setRecordsWithExternalWallet(el, {
|
||
session: wallet.session, name: editorName, records,
|
||
onProgress: (step) => push(step),
|
||
})
|
||
: await BNS.setRecordsWithBuiltInWallet(el, {
|
||
wallet, name: editorName, records,
|
||
onProgress: (step) => push(step),
|
||
});
|
||
push("broadcast: " + (res.txid || res.txId || "(no txid)"));
|
||
msg.className = "msg ok";
|
||
msg.textContent = "Saved. Records will refresh on the chain within a block.";
|
||
$("ed-save").textContent = "Saved ✓";
|
||
// Refresh the names list in the background so the user sees the new
|
||
// records once they land (may take a few seconds via cache/mempool).
|
||
setTimeout(() => loadNames(), 2500);
|
||
} catch (e) {
|
||
msg.className = "msg err";
|
||
msg.textContent = "Failed: " + (e.message || e);
|
||
push("error: " + (e.message || e));
|
||
$("ed-save").disabled = false;
|
||
$("ed-save").textContent = "Save records →";
|
||
} finally {
|
||
try { el?.close?.(); } catch {}
|
||
}
|
||
});
|
||
|
||
// ---------- TLD register (chipnet MVP) ----------
|
||
// Simple oracle: static chipnet rate for testing. Replace with a live feed
|
||
// (CoinGecko, Kraken, or a Chainlink adapter) before mainnet. The `sats` output
|
||
// is what actually gets locked into a service-fee output on the TREG transaction.
|
||
const CHIPNET_SATS_PER_USD = 250_000; // 1 BCH ≈ $400 test rate
|
||
function usdToSats(usd) {
|
||
const n = Number(String(usd).replace(/[^\d.]/g, "")) || 0;
|
||
return Math.max(0, Math.round(n * CHIPNET_SATS_PER_USD));
|
||
}
|
||
|
||
function updateTldPrice() {
|
||
const usd = $("tld-usd").value.trim() || "0";
|
||
const sats = usdToSats(usd);
|
||
$("tld-price").innerHTML = `= <b>${sats.toLocaleString()}</b> sats · rate <b>${CHIPNET_SATS_PER_USD.toLocaleString()}</b> sats/USD (chipnet)`;
|
||
$("tld-submit").textContent = `Register (~${sats.toLocaleString()} sats)`;
|
||
}
|
||
|
||
function validateTldLabel(label) {
|
||
return /^[a-z0-9-]{1,16}$/.test(label);
|
||
}
|
||
|
||
function updateTldButtons() {
|
||
const label = $("tld-label").value.trim().toLowerCase();
|
||
const valid = validateTldLabel(label);
|
||
$("tld-check").disabled = !valid || !wallet;
|
||
$("tld-submit").disabled = !valid || !wallet;
|
||
}
|
||
|
||
$("tld-label").addEventListener("input", () => {
|
||
// normalise as user types
|
||
$("tld-label").value = $("tld-label").value.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 16);
|
||
updateTldButtons();
|
||
});
|
||
$("tld-usd").addEventListener("input", updateTldPrice);
|
||
|
||
$("tld-check").addEventListener("click", async () => {
|
||
const label = $("tld-label").value.trim();
|
||
const msg = $("tld-msg"); msg.textContent = ""; msg.className = "";
|
||
try {
|
||
const r = await fetch("https://navigate.st/api/tlds");
|
||
if (!r.ok) throw new Error("api " + r.status);
|
||
const j = await r.json();
|
||
const taken = (j.tlds || []).some((t) => (t.tld || t) === label);
|
||
msg.className = taken ? "err" : "ok";
|
||
msg.textContent = taken ? `❌ .${label} is already registered` : `✅ .${label} is available`;
|
||
msg.style.color = taken ? "var(--taken)" : "var(--ok)";
|
||
} catch (e) {
|
||
msg.textContent = "could not check: " + (e.message || e);
|
||
msg.style.color = "var(--warn)";
|
||
}
|
||
});
|
||
|
||
$("tld-submit").addEventListener("click", async () => {
|
||
if (!wallet) return;
|
||
const label = $("tld-label").value.trim();
|
||
if (!validateTldLabel(label)) return;
|
||
const sats = BigInt(usdToSats($("tld-usd").value.trim() || "0"));
|
||
const msg = $("tld-msg"); msg.textContent = ""; msg.className = "";
|
||
const log = $("tld-log"); log.style.display = "block"; log.innerHTML = "";
|
||
const push = (t) => { const d = document.createElement("div"); d.textContent = "› " + t; log.appendChild(d); log.scrollTop = log.scrollHeight; };
|
||
$("tld-submit").disabled = true;
|
||
$("tld-submit").textContent = "Signing…";
|
||
let el = null;
|
||
try {
|
||
el = await BNS.connect();
|
||
push("connected to chipnet");
|
||
const serviceFee = {
|
||
address: BNS.REGISTRAR?.serviceFee?.address ?? null,
|
||
sats,
|
||
};
|
||
const res = wallet.source === "wc"
|
||
? await BNS.registerTldWithExternalWallet(el, {
|
||
session: wallet.session,
|
||
tld: label,
|
||
records: {},
|
||
serviceFee,
|
||
tldListUrl: "https://navigate.st/api/tlds",
|
||
onProgress: (step) => push(step),
|
||
})
|
||
: await BNS.registerTldWithBuiltInWallet(el, {
|
||
wallet,
|
||
tld: label,
|
||
records: {},
|
||
serviceFee,
|
||
tldListUrl: "https://navigate.st/api/tlds",
|
||
onProgress: (step) => push(step),
|
||
});
|
||
push("broadcast: " + (res.txid || res.txId || "(no txid)"));
|
||
msg.style.color = "var(--ok)";
|
||
msg.textContent = `✅ .${label} minted. Category ${(res.category || "").slice(0, 16)}…`;
|
||
$("tld-submit").textContent = "Registered ✓";
|
||
// Trigger a name-list refresh — new TLD cert is an NFT held by this wallet.
|
||
setTimeout(() => loadNames(), 2500);
|
||
} catch (e) {
|
||
msg.style.color = "var(--taken)";
|
||
const raw = String(e.message || e);
|
||
// The most common failure by far: wallet has no chipnet coins. Turn the
|
||
// opaque error into a nudge toward a faucet.
|
||
if (/wallet is empty/i.test(raw)) {
|
||
msg.innerHTML = `Failed: wallet is empty. Fund <code style="font-size:11.5px">${esc(wallet.address)}</code>
|
||
with chipnet coins first — free from
|
||
<a href="https://tbch.googol.cash/" target="_blank" rel="noopener">tbch.googol.cash</a> or
|
||
<a href="https://chipnet.bch.ninja/faucet" target="_blank" rel="noopener">chipnet.bch.ninja/faucet</a>.`;
|
||
} else {
|
||
msg.textContent = "Failed: " + raw;
|
||
}
|
||
push("error: " + raw);
|
||
$("tld-submit").disabled = false;
|
||
updateTldPrice(); // reset button label
|
||
} finally {
|
||
try { el?.close?.(); } catch {}
|
||
}
|
||
});
|
||
// Initial paint on page load so the card shows the price even before sign-in.
|
||
updateTldPrice();
|
||
</script>
|
||
<script defer src="./js/site-footer.js?v=20260907rel"></script>
|
||
<script type="module" src="./js/register-flow.js?v=20260908tabs"></script>
|
||
<script defer src="./js/profile-menu.js?v=20260908tabs"></script>
|
||
</body>
|
||
</html>
|