Ariadne's resolver copy and the Android indexer still admitted every REG under a cosign or hidden TLD, so they disagreed with the gateway and Theseus about which names exist. Ariadne now carries the current Argus resolver-web.js verbatim (the copy had drifted: no TLD beacon, no owner tracking, old electrum list); its daemon only imports buildIndex and normalizeName, both unchanged. Bns.java gains the same rule in Java: walk the TLD beacon, keep each TLD's TUPD policy timeline, and drop a REG whose TLD required a co-signature at that height unless the transaction re-issues the TLD certificate. If the TLD beacon cannot be read the mobile index degrades open rather than empty. Needs an APK build (0.19) and an Ariadne zip rebuild to reach devices. Dashboard balance: it was read once from the gateway at sign-in and never again, so payments and spends never showed. The sidebar and Settings figures now come straight from electrum every 15 seconds while the tab is visible, after every broadcast the page makes, on tab focus and on a new refresh button; unconfirmed coins count as spendable (listunspent includes the mempool) with a small "incl. N sat unconfirmed" line. The header wallet pill asks the dashboard for a fresh figure when opened, and the register flow keeps its stored figure current while it watches for funding.
332 lines
18 KiB
JavaScript
332 lines
18 KiB
JavaScript
// Transforms the "🔑 Sign in" nav pill on every Sirius.X page into a wallet
|
|
// dropdown. Reads siriusProfile from localStorage; when null shows the three
|
|
// onboarding actions (New wallet / Import a phrase / WizardConnect), when
|
|
// present shows the signed-in menu (address, My names, Admin, Sign out).
|
|
//
|
|
// Each onboarding action deep-links portal.html?mode=new|import|wc; portal.html
|
|
// picks that up and immediately runs the matching flow — no extra click on
|
|
// arrival. Signed-in state changes flip the button label without a reload:
|
|
// the storage event covers cross-tab, a custom 'siriusProfileChanged' event
|
|
// covers same-tab (portal.html and tld.html both dispatch it on write/clear).
|
|
|
|
(() => {
|
|
const anchor = document.querySelector(".topnav a.portal");
|
|
if (!anchor) return;
|
|
|
|
// Base URL for local links — the nav's brand link is already set with the
|
|
// right relative path per page (./ at root, ../ in subdirs). Reuse it so
|
|
// absolute /sirius-x/... paths (which break under the BCNR route) never
|
|
// slip in.
|
|
const PORTAL_URL = anchor.getAttribute("href") || "./portal.html";
|
|
const BASE = PORTAL_URL.replace(/portal\.html.*$/, "");
|
|
const ADMIN_URL = BASE + "admin/";
|
|
|
|
const readProfile = () => {
|
|
try { return JSON.parse(localStorage.getItem("siriusProfile") || "null"); } catch { return null; }
|
|
};
|
|
|
|
// Build wrapper + button + (empty) menu around the existing pill.
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "profile-wrap";
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = anchor.className; // reuse .portal (and .here if present)
|
|
btn.setAttribute("aria-haspopup", "menu");
|
|
btn.setAttribute("aria-expanded", "false");
|
|
const menu = document.createElement("div");
|
|
menu.className = "profile-menu";
|
|
menu.setAttribute("role", "menu");
|
|
menu.hidden = true;
|
|
// Header "Dashboard" button — sits left of the wallet pill and is the one
|
|
// way into names, TLDs, the web-builder and (for the operator) the admin
|
|
// panel. Always visible: signed out it lands on the portal's sign-in.
|
|
const dash = document.createElement("a");
|
|
dash.className = "dash-link" + (/\/portal\.html(?:$|[?#])/.test(location.pathname) ? " here" : "");
|
|
dash.href = PORTAL_URL;
|
|
dash.innerHTML = `<span aria-hidden="true">▦</span> Dashboard`;
|
|
const readWalletInfo = () => {
|
|
try { return JSON.parse(localStorage.getItem("siriusWalletInfo") || "null"); } catch { return null; }
|
|
};
|
|
const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
|
|
// ---------- render ----------
|
|
// Re-renders the button label AND the menu innerHTML from the current
|
|
// profile in localStorage. Called at load, on every open(), and whenever
|
|
// another tab or this tab updates the storage key.
|
|
function render() {
|
|
const p = readProfile();
|
|
if (p) {
|
|
const dot = `<span class="wallet-dot ok" aria-hidden="true"></span>`;
|
|
const label = shortAddr(p.address);
|
|
const sourceBadge = p.source === "wc"
|
|
? `<span class="wallet-src">WC</span>`
|
|
: `<span class="wallet-src">seed</span>`;
|
|
btn.innerHTML = `${dot}<span class="wallet-label">${escapeAttr(label)}</span>${sourceBadge}<span class="wallet-caret" aria-hidden="true">▾</span>`;
|
|
btn.setAttribute("aria-label", `Wallet ${label}, signed in via ${p.source || "seed"}`);
|
|
// Wallet-only menu: identity, balance, receive, settings, sign out.
|
|
// Names/TLDs/admin live behind the Dashboard button next to this one.
|
|
const info = readWalletInfo();
|
|
const bal = info?.sats != null ? `${fmtInt(info.sats)} sat` : "—";
|
|
const held = info ? `${info.names ?? 0} name${info.names === 1 ? "" : "s"} · ${info.tlds ?? 0} TLD${info.tlds === 1 ? "" : "s"}` : "open the dashboard to load";
|
|
menu.innerHTML = `
|
|
<div class="profile-header">
|
|
<span class="profile-hint">Signed in via ${escapeAttr(p.source === "wc" ? "WizardConnect" : "seed phrase")} · chipnet</span>
|
|
<code>${escapeAttr(p.address)}</code>
|
|
</div>
|
|
<div class="wallet-stats">
|
|
<div><span class="ws-k">Balance</span><span class="ws-v">${escapeAttr(bal)}</span></div>
|
|
<div><span class="ws-k">Holdings</span><span class="ws-v">${escapeAttr(held)}</span></div>
|
|
</div>
|
|
<a href="#" role="menuitem" data-action="copy" data-copy="${escapeAttr(p.address)}">
|
|
<span class="mi-ic">⧉</span>
|
|
<span class="mi-body"><b>Copy address</b><span class="mi-hint">receive coins or names here</span></span>
|
|
</a>
|
|
${p.tokenAddress ? `<a href="#" role="menuitem" data-action="copy" data-copy="${escapeAttr(p.tokenAddress)}">
|
|
<span class="mi-ic">🎟</span>
|
|
<span class="mi-body"><b>Copy token address</b><span class="mi-hint">where name certificates are minted</span></span>
|
|
</a>` : ""}
|
|
<a href="https://tbch.googol.cash/" target="_blank" rel="noopener" role="menuitem">
|
|
<span class="mi-ic">🚰</span>
|
|
<span class="mi-body"><b>Get test coins</b><span class="mi-hint">chipnet faucet</span></span>
|
|
</a>
|
|
<a href="${PORTAL_URL}#settings" role="menuitem">
|
|
<span class="mi-ic">⚙</span>
|
|
<span class="mi-body"><b>Wallet settings</b><span class="mi-hint">session, PIN, forget device</span></span>
|
|
</a>
|
|
<hr>
|
|
<a href="#" role="menuitem" data-action="signout">
|
|
<span class="mi-ic">🚪</span>
|
|
<span class="mi-body"><b>Sign out</b><span class="mi-hint">clear this browser</span></span>
|
|
</a>
|
|
`;
|
|
} else {
|
|
const dot = `<span class="wallet-dot" aria-hidden="true"></span>`;
|
|
btn.innerHTML = `${dot}<span class="wallet-label">Wallet</span><span class="wallet-caret" aria-hidden="true">▾</span>`;
|
|
btn.setAttribute("aria-label", "Wallet — not signed in");
|
|
// If a saved encrypted wallet already exists in this browser (created
|
|
// in a previous session), surface an Unlock option as the first item so
|
|
// users can sign back into the same wallet the mint flow would use —
|
|
// instead of hunting for it inside the Register modal's wallet-choice.
|
|
const hasSaved = (() => {
|
|
try { return !!localStorage.getItem("bns.wallet.v1"); } catch { return false; }
|
|
})();
|
|
const unlockItem = hasSaved ? `
|
|
<a href="#" role="menuitem" data-action="unlock">
|
|
<span class="mi-ic">🔓</span>
|
|
<span class="mi-body"><b>Unlock my wallet</b><span class="mi-hint">one you already created on this device</span></span>
|
|
</a>
|
|
<hr>
|
|
` : "";
|
|
const headerHint = hasSaved
|
|
? `<span class="profile-hint">Locked wallet on this device</span><span class="profile-sub">Unlock or pick another action</span>`
|
|
: `<span class="profile-hint">No wallet yet</span><span class="profile-sub">Pick how to sign in</span>`;
|
|
// Four direct actions — each opens the shared modal at the matching
|
|
// step (Unlock/Create/Import/WizardConnect) with tabs across the top
|
|
// of the modal so the user can switch between them without going back
|
|
// to a landing screen.
|
|
menu.innerHTML = `
|
|
<div class="profile-header">${headerHint}</div>
|
|
${unlockItem}
|
|
<a href="#" role="menuitem" data-action="new">
|
|
<span class="mi-ic">🆕</span>
|
|
<span class="mi-body"><b>Create a wallet</b><span class="mi-hint">generate a fresh 12-word phrase (sign up)</span></span>
|
|
</a>
|
|
<a href="#" role="menuitem" data-action="import">
|
|
<span class="mi-ic">📥</span>
|
|
<span class="mi-body"><b>Import a wallet</b><span class="mi-hint">seed phrase you already have (sign in)</span></span>
|
|
</a>
|
|
<a href="#" role="menuitem" data-action="wc">
|
|
<span class="mi-ic">🔗</span>
|
|
<span class="mi-body"><b>WizardConnect</b><span class="mi-hint">Cashonize / Paytaca via QR</span></span>
|
|
</a>
|
|
`;
|
|
}
|
|
}
|
|
render();
|
|
|
|
// ---------- open/close ----------
|
|
// Opening the menu asks the dashboard (when it is the current page) for a
|
|
// fresh electrum balance; the numbers repaint when it arrives. Other pages
|
|
// show the last figure the dashboard or a sign-in wrote, with its age.
|
|
const open = () => {
|
|
render(); menu.hidden = false; btn.setAttribute("aria-expanded", "true");
|
|
if (typeof window.siriusRefreshBalance === "function") { try { window.siriusRefreshBalance(); } catch {} }
|
|
};
|
|
window.addEventListener("sirius:balance", () => { if (!menu.hidden) render(); });
|
|
const close = () => { menu.hidden = true; btn.setAttribute("aria-expanded", "false"); };
|
|
btn.addEventListener("click", (e) => { e.stopPropagation(); menu.hidden ? open() : close(); });
|
|
document.addEventListener("click", (e) => { if (!wrap.contains(e.target)) close(); });
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
|
|
|
|
// Cross-tab (storage event fires only in other tabs) + same-tab (portal.html
|
|
// dispatches this after writing/clearing) — either wakes render().
|
|
window.addEventListener("storage", (e) => { if (e.key === "siriusProfile") render(); });
|
|
window.addEventListener("siriusProfileChanged", render);
|
|
|
|
// ---------- menu actions ----------
|
|
// Sign-out clears siriusProfile; the wallet-onboarding items (new/import/wc)
|
|
// open the shared mint modal inline (register-flow.js). If the page did not
|
|
// ship register-flow.js in its own <script>, load it on demand.
|
|
const FLOW_URL = "js/register-flow.js?v=20260907signin";
|
|
async function ensureFlow() {
|
|
if (typeof window.siriusSignInWallet === "function") return true;
|
|
try {
|
|
// Same base as the nav portal anchor — works from root pages and
|
|
// subdirs equally (../ vs ./ handled by BASE).
|
|
await import(BASE + FLOW_URL);
|
|
// Its top-level code exposes the global synchronously after the
|
|
// module graph resolves; still, wait a tick for definiteness.
|
|
for (let i = 0; i < 50 && typeof window.siriusSignInWallet !== "function"; i++) {
|
|
await new Promise((r) => setTimeout(r, 30));
|
|
}
|
|
return typeof window.siriusSignInWallet === "function";
|
|
} catch { return false; }
|
|
}
|
|
|
|
menu.addEventListener("click", async (e) => {
|
|
const t = e.target.closest("[data-action]");
|
|
if (!t) return;
|
|
e.preventDefault();
|
|
const action = t.dataset.action;
|
|
if (action === "copy") {
|
|
try { await navigator.clipboard?.writeText(t.dataset.copy || ""); } catch {}
|
|
const b = t.querySelector("b"); const orig = b?.textContent;
|
|
if (b) { b.textContent = "Copied"; setTimeout(() => { b.textContent = orig; }, 1200); }
|
|
return;
|
|
}
|
|
if (action === "signout") {
|
|
try { localStorage.removeItem("siriusProfile"); } catch {}
|
|
// Also wipe the PIN blob so a signed-out device can't be quick-
|
|
// unlocked with a stale PIN. The password-encrypted BuiltInWallet
|
|
// blob (bns.wallet.v1) stays — the user's device, the user's data;
|
|
// they'd remove it explicitly via 'forget wallet' in a future
|
|
// settings screen.
|
|
try { window.siriusPin?.clear(); } catch {}
|
|
try { delete window.siriusWallet; } catch { window.siriusWallet = null; }
|
|
// End the persistent device session too (js/session.js) — otherwise
|
|
// the next visit would sign straight back in.
|
|
try { await window.siriusSession?.forget?.(); } catch {}
|
|
window.dispatchEvent(new Event("siriusProfileChanged"));
|
|
// Reload so any signed-in section on the current page (portal, tld) resets.
|
|
location.reload();
|
|
return;
|
|
}
|
|
if (["new", "import", "wc", "unlock"].includes(action)) {
|
|
close();
|
|
// Always route to the portal — it renders the tabbed sign-in INLINE
|
|
// as page content (?mode=X picks the starting tab). Prevents the
|
|
// "modal opens on top of the page" symptom the launcher used to have.
|
|
// Only stay on-page if we're already on portal.html.
|
|
const onPortal = /\/portal\.html(?:$|[?#])/.test(location.pathname + location.search);
|
|
if (onPortal && typeof window.siriusRenderSignInInline === "function") {
|
|
const hasSaved = (() => { try { return !!localStorage.getItem("bns.wallet.v1"); } catch { return false; } })();
|
|
const mode = (action === "unlock" && !hasSaved) ? "import" : action;
|
|
const mount = document.getElementById("signin-inline");
|
|
if (mount) {
|
|
window.siriusRenderSignInInline(mount, mode);
|
|
document.getElementById("signin")?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
return;
|
|
}
|
|
}
|
|
// Not on portal (or the inline mount is missing) — take the user
|
|
// there. portal.html?mode=X renders the tabbed sign-in as its main
|
|
// content, no modal.
|
|
location.href = PORTAL_URL + "?mode=" + action;
|
|
}
|
|
});
|
|
|
|
wrap.appendChild(btn);
|
|
wrap.appendChild(menu);
|
|
anchor.replaceWith(wrap);
|
|
wrap.parentNode.insertBefore(dash, wrap);
|
|
render(); // dash visibility depends on the profile
|
|
|
|
// ---------- inline CSS ----------
|
|
if (!document.getElementById("profile-menu-css")) {
|
|
const s = document.createElement("style");
|
|
s.id = "profile-menu-css";
|
|
s.textContent = `
|
|
.topnav .profile-wrap{position:relative;margin-left:auto}
|
|
.topnav .dash-link{margin-left:auto;display:inline-flex;align-items:center;gap:6px;
|
|
padding:6px 12px;border-radius:999px;font-size:13.5px;font-weight:500;text-decoration:none;
|
|
color:var(--ink,#f1f4fa);background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12)}
|
|
.topnav .dash-link:hover{background:rgba(255,255,255,.1)}
|
|
.topnav .dash-link.here{color:var(--acid,#d6ff3d);border-color:rgba(214,255,61,.35);background:rgba(214,255,61,.08)}
|
|
.topnav .dash-link[hidden]{display:none}
|
|
.topnav .dash-link + .profile-wrap{margin-left:8px}
|
|
.topnav .lang-picker + .dash-link{margin-left:8px}
|
|
.topnav .profile-menu .wallet-stats{display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:6px 4px 10px;border-bottom:1px solid rgba(255,255,255,.08);margin-bottom:4px}
|
|
.topnav .profile-menu .wallet-stats > div{background:rgba(255,255,255,.04);border-radius:9px;padding:8px 10px;display:flex;flex-direction:column;gap:2px}
|
|
.topnav .profile-menu .ws-k{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:#8b98a9;font-weight:600}
|
|
.topnav .profile-menu .ws-v{font-size:12.5px;color:#e7eaf1;font-family:ui-monospace,monospace}
|
|
.topnav .profile-wrap button.portal{
|
|
display:inline-flex;align-items:center;gap:8px;
|
|
cursor:pointer;font-family:inherit;font-size:13.5px;font-weight:500;
|
|
padding:6px 12px 6px 10px;border-radius:999px;
|
|
background:rgba(214,255,61,.10);color:var(--acid);
|
|
border:1px solid rgba(214,255,61,.22);transition:background .1s,border-color .1s
|
|
}
|
|
.topnav .profile-wrap button.portal:hover{background:rgba(214,255,61,.16);border-color:rgba(214,255,61,.35)}
|
|
.topnav .profile-wrap button.portal[aria-expanded="true"]{background:rgba(214,255,61,.20)}
|
|
.topnav .profile-wrap .wallet-dot{
|
|
width:7px;height:7px;border-radius:50%;background:rgba(139,152,169,.45);
|
|
box-shadow:0 0 0 2px rgba(139,152,169,.10)
|
|
}
|
|
.topnav .profile-wrap .wallet-dot.ok{
|
|
background:#4fd1a5;box-shadow:0 0 0 2px rgba(79,209,165,.20)
|
|
}
|
|
.topnav .profile-wrap .wallet-label{font-family:ui-monospace,monospace;font-size:12.5px;letter-spacing:.2px}
|
|
.topnav .profile-wrap .wallet-src{
|
|
font-size:9.5px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;
|
|
padding:2px 6px;border-radius:4px;background:rgba(214,255,61,.15);color:var(--acid);
|
|
margin-left:2px
|
|
}
|
|
.topnav .profile-wrap .wallet-caret{opacity:.6;font-size:11px;margin-left:2px}
|
|
|
|
.topnav .profile-menu{
|
|
position:absolute;right:0;top:calc(100% + 8px);
|
|
min-width:280px;z-index:30;
|
|
background:rgba(18,24,34,.98);backdrop-filter:blur(12px);
|
|
border:1px solid rgba(255,255,255,.12);border-radius:14px;
|
|
padding:8px;display:flex;flex-direction:column;gap:2px;
|
|
box-shadow:0 12px 40px rgba(0,0,0,.55),0 0 0 1px rgba(0,0,0,.4)
|
|
}
|
|
.topnav .profile-menu[hidden]{display:none}
|
|
.topnav .profile-menu > a{
|
|
display:grid;grid-template-columns:24px 1fr;gap:12px;align-items:center;
|
|
padding:9px 11px;border-radius:9px;
|
|
color:#e7eaf1;text-decoration:none;transition:background .08s
|
|
}
|
|
.topnav .profile-menu > a:hover{background:rgba(214,255,61,.10)}
|
|
.topnav .profile-menu > a:hover .mi-ic{filter:brightness(1.2)}
|
|
.topnav .profile-menu .mi-ic{font-size:17px;line-height:1;text-align:center}
|
|
.topnav .profile-menu .mi-body{display:flex;flex-direction:column;gap:1px;font-size:13.5px;font-weight:600;color:#e7eaf1}
|
|
.topnav .profile-menu .mi-hint{font-weight:400;font-size:11.5px;color:#8b98a9;letter-spacing:.1px}
|
|
.topnav .profile-menu hr{border:0;border-top:1px solid rgba(255,255,255,.08);margin:4px 2px}
|
|
.topnav .profile-menu .profile-header{
|
|
padding:8px 11px 12px;display:flex;flex-direction:column;gap:4px;
|
|
border-bottom:1px solid rgba(255,255,255,.08);margin-bottom:4px
|
|
}
|
|
.topnav .profile-menu .profile-hint{
|
|
color:#8b98a9;font-size:11px;text-transform:uppercase;letter-spacing:.4px;font-weight:600
|
|
}
|
|
.topnav .profile-menu .profile-sub{color:#5e6678;font-size:12px}
|
|
.topnav .profile-menu code{
|
|
font-family:ui-monospace,monospace;font-size:11.5px;
|
|
color:#e7eaf1;word-break:break-all;background:transparent;padding:0;border:0
|
|
}
|
|
`;
|
|
document.head.appendChild(s);
|
|
}
|
|
|
|
// ---------- helpers ----------
|
|
function shortAddr(a) {
|
|
if (!a) return "wallet";
|
|
const i = a.indexOf(":");
|
|
const body = i > 0 ? a.slice(i + 1) : a;
|
|
return body.slice(0, 6) + "…" + body.slice(-4);
|
|
}
|
|
function escapeAttr(s) {
|
|
return String(s).replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]));
|
|
}
|
|
})();
|