The landing kept expanding whenever documentation was added inline. Moving it
to its own subdirectory gives docs room to grow and keeps the landing to the
"try it / roadmap / verify" story it's supposed to be.
* docs/index.html — TOC + eight sections (TLD registry, registration,
records, resolver, mainnet pricing, tracker, verification, runbook).
Reads on its own; no dependency on the landing.
* index.html — #docs section shrunk to a one-paragraph call-to-action with a
prominent "Open the docs →" button; nav Docs link now points at /docs/.
* portal.html + theseus/index.html — nav Docs links updated to /docs/ (was
./#docs / ../#docs) so every subpage points at the standalone docs.
264 lines
12 KiB
HTML
264 lines
12 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.">
|
|
<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)}
|
|
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;gap:12px;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)}
|
|
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="./docs/">📚 Docs</a>
|
|
<a href="./theseus/">🧭 Theseus</a>
|
|
<a href="./portal.html" class="here">🔑 Portal</a>
|
|
<a href="https://silentmode.st/" rel="noopener">Silent Mode ↗</a>
|
|
</nav>
|
|
|
|
<header class="hero">
|
|
<div class="mark">🔑</div>
|
|
<h1>Your <span class="g">names</span> portal</h1>
|
|
<p class="tag">Sign in with your Bitcoin Cash seed phrase or connect an external wallet to see
|
|
every BCNR name your wallet holds. Your keys never leave this browser tab.</p>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
|
|
<section id="signin">
|
|
<h2>Sign in</h2>
|
|
<p class="muted">Pick one. Nothing is transmitted — resolution is a read-only chain query,
|
|
and signing (for record edits) happens locally in the browser or in your external wallet.</p>
|
|
<div class="authbox" style="margin-top:14px">
|
|
<div class="card">
|
|
<h3>Import a seed phrase</h3>
|
|
<p>Paste your 12- or 24-word BIP-39 recovery phrase. It stays in memory only, wiped when you
|
|
close the tab or sign out.</p>
|
|
<label for="seed">Recovery phrase</label>
|
|
<textarea id="seed" placeholder="word1 word2 … word12" spellcheck="false" autocomplete="off"></textarea>
|
|
<div class="row">
|
|
<button class="btn acid" id="signin-btn">Sign in →</button>
|
|
<span class="dim">Chipnet only for now.</span>
|
|
</div>
|
|
<div id="signin-err" class="err hidden"></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Connect an external wallet</h3>
|
|
<p>Use Cashonize 0.9+ or Paytaca via WizardConnect. The wallet signs record edits — the seed
|
|
never touches this page.</p>
|
|
<div class="row">
|
|
<button class="btn ghost" id="wc-btn" disabled>WizardConnect
|
|
<span class="badge b-warn" style="margin-left:6px">soon</span></button>
|
|
</div>
|
|
<p class="dim" style="margin-top:8px">Portal-side WizardConnect wiring is in progress; the
|
|
register.html flow already ships the WizardConnect adapter, and the portal will reuse it.</p>
|
|
</div>
|
|
</div>
|
|
<div class="status" style="margin-top:14px">
|
|
Don't have a wallet? Create one during
|
|
<a href="https://silentmode.st/sirius/register.html">name registration</a> — the recovery phrase
|
|
is generated in your browser and encrypted with your password.
|
|
</div>
|
|
</section>
|
|
|
|
<section id="me" class="hidden">
|
|
<h2>Signed in</h2>
|
|
<div class="card">
|
|
<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>
|
|
|
|
<footer>
|
|
Your keys, your names, your browser. ·
|
|
<a href="./">back to sirius.x</a> ·
|
|
<a href="https://silentmode.st/sirius/register.html">register a new name</a>
|
|
</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.
|
|
import * as BNS from "https://silentmode.st/js/bns-register.js";
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
|
|
|
let wallet = null;
|
|
|
|
// ---------- sign in ----------
|
|
$("signin-btn").addEventListener("click", async () => {
|
|
const btn = $("signin-btn");
|
|
const errBox = $("signin-err");
|
|
errBox.classList.add("hidden");
|
|
const raw = $("seed").value.trim();
|
|
if (!raw) { errBox.textContent = "Enter your seed phrase to sign in."; errBox.classList.remove("hidden"); return; }
|
|
btn.disabled = true; btn.textContent = "Unlocking…";
|
|
try {
|
|
wallet = await BNS.BuiltInWallet.fromMnemonic(raw);
|
|
} catch (e) {
|
|
errBox.textContent = "Could not unlock: " + (e.message || e);
|
|
errBox.classList.remove("hidden");
|
|
btn.disabled = false; btn.textContent = "Sign in →";
|
|
return;
|
|
}
|
|
// Wipe the textarea immediately — the seed is now in the wallet object only.
|
|
$("seed").value = "";
|
|
btn.disabled = false; btn.textContent = "Sign in →";
|
|
await enterPortal();
|
|
});
|
|
|
|
$("signout-btn").addEventListener("click", () => {
|
|
wallet = null;
|
|
$("me").classList.add("hidden");
|
|
$("signin").classList.remove("hidden");
|
|
$("names-list").innerHTML = "";
|
|
$("names-status").textContent = "Loading names from the chain…";
|
|
});
|
|
|
|
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;
|
|
await loadNames();
|
|
}
|
|
|
|
// ---------- load names ----------
|
|
// Approach: build the full BCNR index once (small on chipnet), then filter to
|
|
// names whose category matches one of the token UTXOs the wallet holds across
|
|
// its watched addresses. This mirrors what portfolio.js does server-side.
|
|
async function loadNames() {
|
|
const status = $("names-status");
|
|
const list = $("names-list");
|
|
list.innerHTML = "";
|
|
status.className = "status";
|
|
status.textContent = "Loading names from the chain…";
|
|
try {
|
|
// Fetch the wallet's token UTXOs across all watched addresses.
|
|
const addresses = wallet.watchedAddresses;
|
|
const utxos = await BNS.getUtxosForAddresses(addresses);
|
|
const ownedCategories = new Set(
|
|
utxos.filter((u) => u.token && u.token.nft).map((u) => u.token.category || u.token.tokenId)
|
|
);
|
|
status.textContent = `Found ${ownedCategories.size} NFT(s) across ${addresses.length} addresses. Cross-checking against the name registry…`;
|
|
|
|
// Walk the BCNR index and keep entries whose category the wallet holds.
|
|
const index = await BNS.buildIndex({ WebSocket });
|
|
const mine = [];
|
|
for (const entry of index.values()) if (ownedCategories.has(entry.category)) mine.push(entry);
|
|
|
|
if (!mine.length) {
|
|
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 ${mine.length} name${mine.length === 1 ? "" : "s"}.`;
|
|
for (const e of mine.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
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>`;
|
|
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>` +
|
|
`<div class="rec">${records}</div>`;
|
|
list.appendChild(div);
|
|
}
|
|
} catch (e) {
|
|
status.className = "status err";
|
|
status.textContent = "Could not load names from the chain: " + (e.message || e);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|