sirius.x: fix electrum listunspent + profile dropdown across all pages

registrar.js: BNS.connect() now filters silentmode.st/electrum and
coinspectrum.duckdns.org:50011 out of the default electrum list. Those
endpoints are our own bns-indexer.js — beacon-only, serves get_history +
transaction.get but NOT listunspent for arbitrary scripthashes. Every
wallet op (getBalance, getUtxos, edit signing) needs listunspent, so
picking a beacon indexer first (which we did for best reachability) broke
every wallet unlock with "-32601 unsupported method: blockchain.
scripthash.listunspent". New `beaconOk: true` opts back in for pure-
resolution paths.

Rebuilt the browser bundle (site/js/bns-register.js) so the fix reaches
portal + admin + register.html + anything else that imports BNS.connect.

site-sirius-x/js/profile-menu.js: a small shared script that transforms
the "🔑 Sign in" nav pill into a dropdown menu on every sirius.x page.
Signed-out shows New wallet / Add wallet / WizardConnect + a link to the
sign-in page. Signed-in (reads localStorage 'siriusProfile') shows the
short address, My names, Admin, Sign out. Included via one <script defer>
tag on each of the 6 pages (landing / portal / admin / docs / theseus /
brand); dropdown CSS is inlined by the script itself so consumers don't
need a matching stylesheet.

Portal.html writing to localStorage.'siriusProfile' after sign-in is a
follow-up so the dropdown reflects state across pages — until then the
menu always shows the onboarding options.
This commit is contained in:
Local Dev 2026-09-06 18:53:17 +02:00
parent 22ff0e0167
commit e47413eeb5

View file

@ -14,10 +14,12 @@ import {
} from "./chain.js";
import {
buildRegistrationTx,
buildTldRegistrationTx,
buildUpdateTx,
buildPrepTx,
hasGenesisInput,
normalizeName,
normalizeTld,
displayName,
payloadBudget,
toWcTransactionObject,
@ -26,9 +28,27 @@ import { signTransaction, txidOfHex } from "./sign-tx.js";
import { buildIndex } from "./resolver-web.js";
import { REGISTRAR } from "../registrar-config.js";
/** Open a connection to the chain. Caller must `.close()` it. */
export async function connect({ electrum = REGISTRAR.electrum, WebSocket } = {}) {
return connectAny(electrum, WebSocket);
// URLs of "beacon-only" indexers (our own bns-indexer.js). They serve
// get_history + transaction.get for the name and TLD beacons but NOT
// listunspent for arbitrary scripthashes, so a wallet op that lands on one
// fails with "-32601 unsupported method: blockchain.scripthash.listunspent".
// Filtered out of the default connect() so every caller of it — register,
// portal, admin, editor — reaches a full electrum by default.
const BEACON_ONLY_URLS = /silentmode\.st\/electrum|coinspectrum\.duckdns\.org:50011/;
/**
* Open a connection to the chain. Caller must `.close()` it.
*
* By default the beacon-only indexers (silentmode.st/electrum,
* coinspectrum.duckdns.org:50011) are filtered out so wallet ops always land
* on a full electrum. For pure-name-resolution paths that only need
* `blockchain.scripthash.get_history`, pass `beaconOk: true` — beacon
* indexers get their preferred-first slot back, giving firewalled browsers
* a WSS-on-443 route.
*/
export async function connect({ electrum = REGISTRAR.electrum, WebSocket, beaconOk = false } = {}) {
const list = beaconOk ? electrum : electrum.filter((u) => !BEACON_ONLY_URLS.test(typeof u === "string" ? u : (u.url || "")));
return connectAny(list, WebSocket);
}
/** Is this name still free? Reads the chain, not a database. */
@ -164,6 +184,115 @@ export async function registerWithBuiltInWallet(client, {
}
/**
* Register a TLD certificate on the TLD-registry beacon. Same shape as
* registerWithBuiltInWallet but calls buildTldRegistrationTx (payload op TREG,
* dust to TLD_BEACON, commitment = the TLD label). Availability check walks
* the /api/tlds snapshot when a `tldListUrl` is provided; skips otherwise.
*
* @param {object} o.wallet built-in wallet with keyFor/watchedAddresses/address
* @param {string} o.tld a bare TLD label, e.g. "vpn" (1–16 chars, [a-z0-9-])
* @param {object} [o.records] TLD-cert records (policy, fee_bps, reserved, …)
* @param {object} [o.serviceFee] {address, sats} — the $5-in-sats fee output
* @param {string} [o.tldListUrl] endpoint to fetch existing TLDs from (e.g.
* https://navigate.st/api/tlds). Availability
* check is skipped if not provided.
*/
export async function registerTldWithBuiltInWallet(client, {
wallet,
tld,
records = {},
serviceFee = { address: null, sats: 0n },
feeRate = REGISTRAR.feeRate,
tldListUrl = null,
onProgress = () => {},
}) {
const rawTld = normalizeTld(tld);
onProgress("checking-availability");
if (tldListUrl) {
try {
const r = await fetch(tldListUrl);
if (r.ok) {
const j = await r.json();
const taken = Array.isArray(j.tlds) && j.tlds.some((t) => (t.tld || t) === rawTld);
if (taken) throw new Error(`".${rawTld}" is already registered as a TLD`);
}
} catch (e) {
if (/already registered/.test(e.message)) throw e;
// network / API blip — proceed; on-chain "first TREG wins" rule handles
// collisions and the client-side check is a UX assist, not a gate.
}
}
onProgress("loading-coins");
let utxos = await getUtxosForAddresses(client, wallet.watchedAddresses);
const spendable = utxos.filter((u) => !u.token).reduce((s, u) => s + u.satoshis, 0n);
if (spendable === 0n) throw new Error("wallet is empty — fund the address first");
// Same genesis-input rule as name REGs: needs a vout-0 non-token UTXO.
let prepTxid = null;
if (!hasGenesisInput(utxos)) {
onProgress("preparing-wallet");
const prep = buildPrepTx({
ownerAddress: wallet.address,
utxos,
feeRate,
addressToLockingBytecode,
});
const signedPrep = signTransaction(prep.transaction, prep.sourceOutputs, (h) => wallet.keyFor(h));
prepTxid = await broadcastTx(client, signedPrep.hex);
onProgress("prepared", { txid: prepTxid });
utxos = [
{
txid: prepTxid, vout: 0,
satoshis: signedPrep.transaction.outputs[0].valueSatoshis,
address: wallet.address,
lockingBytecode: addressToLockingBytecode(wallet.address),
},
...(signedPrep.transaction.outputs.length > 1
? [{
txid: prepTxid, vout: 1,
satoshis: signedPrep.transaction.outputs[1].valueSatoshis,
address: wallet.address,
lockingBytecode: addressToLockingBytecode(wallet.address),
}]
: []),
];
}
onProgress("building");
const built = buildTldRegistrationTx({
tld: rawTld,
ownerAddress: wallet.address,
ownerTokenAddress: toTokenAddress(wallet.address),
changeAddress: wallet.address,
records,
utxos,
feeAddress: serviceFee.address || null,
feeSats: BigInt(serviceFee.sats ?? 0n),
feeRate,
addressToLockingBytecode,
});
onProgress("signing");
const signed = signTransaction(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h));
onProgress("broadcasting");
const txid = await broadcastTx(client, signed.hex);
onProgress("registered", { txid, category: built.category });
return {
tld: built.tld,
category: built.category,
txid,
prepTxid,
ownerAddress: wallet.address,
certificateAddress: toTokenAddress(wallet.address),
costs: built.costs,
hex: signed.hex,
};
}
/**
* Mint a name certificate on a buyer's behalf, from the registrar's own coins.
*