feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
// Sirius.X dashboard — the signed-in control panel for names and TLDs.
//
// One page, a left menu, several panes: Overview, Domain names (with a
// per-name detail: DNS records, content & hosting, redirect, transfer,
// export), My TLD list (with a per-TLD detail), Wallet, Settings.
//
// Trust model is unchanged from the old portal: the wallet lives in this
// browser; every on-chain change is a transaction signed here; DNS records
// are a manifest signed here and stored on Sia via the gateway. What is new
// is the persistent device session (js/session.js): sign in once, come back
// without a prompt until you sign out.
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market" ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
const API = "https://silentmode.st" ;
const TLD _API = "https://navigate.st/api/tlds?include_hidden=1" ;
2026-09-17 03:28:50 +02:00
// Operator wallet (chipnet): shows the Admin panel link in the sidebar.
const OPERATOR _ADDRESS = "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu" ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
const $ = ( id ) => document . getElementById ( id ) ;
const esc = ( s ) => String ( s ? ? "" ) . replace ( /[&<>"']/g , ( c ) => ( { "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ , "'" : "'" } [ c ] ) ) ;
const fmtInt = ( n ) => String ( n ) . replace ( /\B(?=(\d{3})+(?!\d))/g , "," ) ;
const shortAddr = ( a ) => ( a ? a . replace ( /^[^:]+:/ , "" ) . slice ( 0 , 8 ) + "…" + a . slice ( - 6 ) : "—" ) ;
const sleep = ( ms ) => new Promise ( ( r ) => setTimeout ( r , ms ) ) ;
// ---------- state ----------
let wallet = null ;
const H = { names : [ ] , tlds : [ ] , wallet : null , chain : null , loadedAt : 0 } ;
const dnsCache = new Map ( ) ; // name -> { status: "none"|"ok"|"nopointer"|"error", seq, dns, raw }
let registryCache = { at : 0 , names : [ ] } ;
let current = { name : null , entry : null , manifestSeq : 0 , rows : [ ] , dirty : false } ;
let currentTld = null ;
// ---------- profile breadcrumb (shared with profile-menu.js) ----------
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 readProfile ( ) {
try { return JSON . parse ( localStorage . getItem ( "siriusProfile" ) || "null" ) ; } catch { return null ; }
}
function clearProfile ( ) {
try { localStorage . removeItem ( "siriusProfile" ) ; } catch { }
window . dispatchEvent ( new Event ( "siriusProfileChanged" ) ) ;
}
// ---------- session ----------
async function restoreSession ( ) {
if ( window . siriusWallet ) return window . siriusWallet ;
const S = window . siriusSession ;
if ( ! S ? . restore ) return null ;
try {
const s = await S . restore ( ) ;
if ( ! s ? . mnemonic ) return null ;
const w = await BNS . BuiltInWallet . fromMnemonic ( s . mnemonic , BNS . CHIPNET _PREFIX , s . accountPath || undefined ) ;
window . siriusWallet = w ;
return w ;
} catch { return null ; }
}
async function ensureSignInFlow ( ) {
for ( let i = 0 ; i < 80 && typeof window . siriusRenderSignInInline !== "function" ; i ++ ) await sleep ( 50 ) ;
return typeof window . siriusRenderSignInInline === "function" ;
}
async function paintInlineSignIn ( mode ) {
if ( ! ( await ensureSignInFlow ( ) ) ) return ;
const hasSaved = ( ( ) => { try { return ! ! localStorage . getItem ( "bns.wallet.v1" ) ; } catch { return false ; } } ) ( ) ;
window . siriusRenderSignInInline ( $ ( "signin-inline" ) , mode || ( hasSaved ? "unlock" : "import" ) ) ;
}
function showSignedOut ( ) {
wallet = null ;
window . siriusWallet = null ;
$ ( "me" ) . classList . add ( "hidden" ) ;
$ ( "signed-out" ) . classList . remove ( "hidden" ) ;
}
async function enter ( w ) {
wallet = w ;
window . siriusWallet = w ;
$ ( "signed-out" ) . classList . add ( "hidden" ) ;
$ ( "me" ) . classList . remove ( "hidden" ) ;
$ ( "side-addr" ) . textContent = w . address ;
$ ( "me-addr" ) . textContent = w . address ;
$ ( "me-taddr" ) . textContent = w . tokenAddress ;
$ ( "me-info" ) . textContent = ` watched addresses: ${ w . watchedAddresses . length } ` ;
$ ( "w-watched" ) . textContent = String ( w . watchedAddresses . length ) ;
2026-09-17 03:28:50 +02:00
// Operator tools are only useful to the operator wallet.
$ ( "side-admin" ) . classList . toggle ( "hidden" , w . address !== OPERATOR _ADDRESS ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
paintSettings ( ) ;
updateTldPrice ( ) ; updateTldButtons ( ) ;
routeFromHash ( ) ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
refreshBalance ( ) ; startBalancePolling ( ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
await loadHoldings ( ) ;
}
async function signOut ( { forget = false } = { } ) {
if ( wallet ? . source === "wc" && wallet . session ) { try { await wallet . session . disconnect ( ) ; } catch { } }
try { await window . siriusSession ? . forget ? . ( ) ; } catch { }
if ( forget ) {
try { BNS . BuiltInWallet . forget ( ) ; } catch { }
try { window . siriusPin ? . clear ? . ( ) ; } catch { }
}
clearProfile ( ) ;
showSignedOut ( ) ;
history . replaceState ( null , "" , location . pathname ) ;
await paintInlineSignIn ( forget ? "import" : "unlock" ) ;
}
window . addEventListener ( "siriusProfileChanged" , async ( ) => {
if ( readProfile ( ) ) { if ( window . siriusWallet && ! wallet ) await enter ( window . siriusWallet ) ; }
else if ( wallet ) { try { await window . siriusSession ? . forget ? . ( ) ; } catch { } showSignedOut ( ) ; await paintInlineSignIn ( ) ; }
} ) ;
window . addEventListener ( "storage" , async ( e ) => {
if ( e . key !== "siriusProfile" ) return ;
if ( ! readProfile ( ) && wallet ) { showSignedOut ( ) ; await paintInlineSignIn ( ) ; }
} ) ;
( async function bootstrap ( ) {
const mode = new URLSearchParams ( location . search ) . get ( "mode" ) ;
if ( window . siriusWallet ) return enter ( window . siriusWallet ) ;
const w = await restoreSession ( ) ;
if ( w ) { writeProfile ( w ) ; return enter ( w ) ; }
await paintInlineSignIn ( mode || undefined ) ;
} ) ( ) ;
// ---------- navigation ----------
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
const PANES = [ "studio" , "domains" , "domain" , "tlds" , "tld" , "newtld" , "settings" ] ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
function go ( pane , arg ) {
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
if ( ! PANES . includes ( pane ) ) pane = "domains" ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
document . querySelectorAll ( ".pane[data-pane]" ) . forEach ( ( p ) => { p . hidden = p . dataset . pane !== pane ; } ) ;
const activeItem = pane === "domain" ? "domains" : pane === "tld" ? "tlds" : pane ;
document . querySelectorAll ( ".side .item[data-go]" ) . forEach ( ( b ) => b . classList . toggle ( "active" , b . dataset . go === activeItem ) ) ;
const hash = arg ? ` # ${ pane } / ${ encodeURIComponent ( arg ) } ` : ` # ${ pane } ` ;
if ( location . hash !== hash ) history . replaceState ( null , "" , hash ) ;
if ( pane === "domain" && arg ) openDomain ( arg ) ;
if ( pane === "tld" && arg ) openTld ( arg ) ;
if ( pane === "tlds" ) renderTlds ( ) ;
if ( pane === "domains" ) renderDomains ( ) ;
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
if ( pane === "studio" ) renderStudio ( ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
window . scrollTo ( { top : 0 , behavior : "smooth" } ) ;
}
function routeFromHash ( ) {
const m = /^#([a-z]+)(?:\/(.+))?$/ . exec ( location . hash || "" ) ;
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
if ( ! m ) return go ( "domains" ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
go ( m [ 1 ] , m [ 2 ] ? decodeURIComponent ( m [ 2 ] ) : undefined ) ;
}
window . addEventListener ( "hashchange" , ( ) => { if ( wallet ) routeFromHash ( ) ; } ) ;
document . addEventListener ( "click" , ( e ) => {
const el = e . target . closest ( "[data-go]" ) ;
if ( ! el ) return ;
e . preventDefault ( ) ;
go ( el . dataset . go , el . dataset . arg ) ;
} ) ;
$ ( "side-signout" ) . addEventListener ( "click" , ( ) => signOut ( ) ) ;
$ ( "set-signout" ) . addEventListener ( "click" , ( ) => signOut ( ) ) ;
$ ( "set-forget" ) . addEventListener ( "click" , async ( ) => {
if ( ! confirm ( "Forget the wallet on this device? You will need your recovery phrase to sign in again." ) ) return ;
await signOut ( { forget : true } ) ;
} ) ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
$ ( "refresh-btn" ) . addEventListener ( "click" , ( ) => { loadHoldings ( ) ; refreshBalance ( ) ; } ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
document . querySelectorAll ( "[data-copy]" ) . forEach ( ( b ) => b . addEventListener ( "click" , ( ) => {
const t = $ ( b . dataset . copy ) ? . textContent || "" ;
navigator . clipboard ? . writeText ( t ) ;
const o = b . textContent ; b . textContent = "Copied" ; setTimeout ( ( ) => { b . textContent = o ; } , 1200 ) ;
} ) ) ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
// ---------- live balance ----------
// Straight from electrum, not the gateway: listunspent includes mempool
// coins, so a payment shows the moment it is relayed — zero-conf is money
// here, same as the register flow. Polled while the tab is visible and
// refreshed right after anything this page broadcasts.
const setText = ( id , v ) => { const el = $ ( id ) ; if ( el ) el . textContent = v ; } ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
const fmtBits = ( sats ) => ( window . siriusPricing ? . formatBits || ( ( s ) => ( Number ( s ) / 100 ) . toLocaleString ( "en-US" ) + " bits" ) ) ( sats ) ;
const fmtBch = ( sats ) => ( window . siriusPricing ? . formatBch || ( ( s ) => ( Number ( s ) / 1e8 ) . toFixed ( 8 ) . replace ( /0+$/ , "" ) . replace ( /\.$/ , "" ) + " BCH" ) ) ( sats ) ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
let balEl = null , balTimer = null , balBusy = false ;
async function refreshBalance ( ) {
if ( ! wallet || balBusy ) return ;
balBusy = true ;
const spinner = $ ( "side-refresh" ) ; spinner ? . classList . add ( "spin" ) ;
try {
if ( ! balEl ) balEl = await BNS . connect ( ) ;
const b = await BNS . getBalance ( balEl , wallet . watchedAddresses ) ;
const pending = b . sats - b . confirmedSats ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
setText ( "side-balance" , fmtBits ( b . sats ) ) ;
setText ( "w-sats" , ` ${ fmtBits ( b . sats ) } · ${ fmtBch ( b . sats ) } ${ pending > 0 n ? ` ( ${ fmtBits ( pending ) } unconfirmed, spendable now) ` : "" } ` ) ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
setText ( "w-utxos" , String ( b . coins ) ) ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
const pend = $ ( "side-pending" ) ; if ( pend ) { pend . hidden = pending <= 0 n ; setText ( "side-pending-sats" , fmtBits ( pending ) ) ; }
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
H . wallet = { ... ( H . wallet || { } ) , sats : b . sats . toString ( ) , utxo _count : b . coins } ;
try { localStorage . setItem ( "siriusWalletInfo" , JSON . stringify ( { sats : b . sats . toString ( ) , utxos : b . coins , names : H . names . length , tlds : H . tlds . length , at : Date . now ( ) } ) ) ; } catch { }
window . dispatchEvent ( new CustomEvent ( "sirius:balance" , { detail : { sats : b . sats . toString ( ) , pending : pending . toString ( ) } } ) ) ;
} catch {
try { balEl ? . close ? . ( ) ; } catch { }
balEl = null ;
} finally { balBusy = false ; spinner ? . classList . remove ( "spin" ) ; }
}
function startBalancePolling ( ) {
clearInterval ( balTimer ) ;
balTimer = setInterval ( ( ) => { if ( document . visibilityState === "visible" ) refreshBalance ( ) ; } , 15000 ) ;
}
document . addEventListener ( "visibilitychange" , ( ) => { if ( document . visibilityState === "visible" && wallet ) refreshBalance ( ) ; } ) ;
window . siriusRefreshBalance = refreshBalance ;
$ ( "side-refresh" ) . addEventListener ( "click" , ( ) => refreshBalance ( ) ) ;
$ ( "names-refresh" ) . addEventListener ( "click" , ( ) => { loadHoldings ( ) ; refreshBalance ( ) ; } ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
// ---------- holdings ----------
async function loadHoldings ( ) {
const status = $ ( "names-status" ) ;
status . className = "status" ; status . textContent = "Loading names from the chain…" ;
try {
const scripthashes = wallet . watchedAddresses . map ( ( a ) => BNS . addressToScripthash ( a ) ) ;
const r = await fetch ( ` ${ API } /api/holdings ` , { method : "POST" , headers : { "content-type" : "application/json" } , body : JSON . stringify ( { scripthashes } ) } ) ;
if ( ! r . ok ) throw new Error ( ` API ${ r . status } : ${ ( await r . text ( ) . catch ( ( ) => "" ) ) . slice ( 0 , 200 ) } ` ) ;
const p = await r . json ( ) ;
H . names = ( p . names || [ ] ) . map ( ( n ) => ( { ... n , label : n . name . split ( "." ) [ 0 ] } ) ) ;
H . tlds = p . tlds || [ ] ;
H . wallet = p . wallet || null ; H . chain = p . chain || null ; H . loadedAt = Date . now ( ) ;
// header numbers
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
const setText = ( id , v ) => { const el = $ ( id ) ; if ( el ) el . textContent = v ; } ;
setText ( "cnt-names" , H . names . length ) ; setText ( "cnt-tlds" , H . tlds . length ) ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
if ( H . wallet ? . sats != null && $ ( "side-balance" ) . textContent === "—" ) {
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
setText ( "side-balance" , fmtBits ( H . wallet . sats ) ) ; setText ( "w-sats" , ` ${ fmtBits ( H . wallet . sats ) } · ${ fmtBch ( H . wallet . sats ) } ` ) ; setText ( "w-utxos" , String ( H . wallet . utxo _count ) ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
}
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
try { const prev = JSON . parse ( localStorage . getItem ( "siriusWalletInfo" ) || "{}" ) ; localStorage . setItem ( "siriusWalletInfo" , JSON . stringify ( { ... prev , sats : prev . sats ? ? String ( H . wallet ? . sats ? ? "" ) , names : H . names . length , tlds : H . tlds . length , at : Date . now ( ) } ) ) ; } catch { }
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
if ( H . chain ? . height != null ) setText ( "stat-height" , fmtInt ( H . chain . height ) ) ;
renderAttention ( ) ; renderDomains ( ) ; renderTlds ( ) ; renderStudio ( ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
if ( current . name ) { const e = H . names . find ( ( n ) => n . name === current . name ) ; if ( e ) { current . entry = e ; fillSummary ( e ) ; } }
warmDnsCache ( ) ;
} catch ( e ) {
status . className = "status err" ;
status . textContent = "Could not load names: " + ( e . message || e ) ;
}
}
// DNS presence per name, fetched in the background (4 at a time) so the
// list can show a badge and Overview can flag names without records.
async function fetchDns ( name , force = false ) {
if ( ! force && dnsCache . has ( name ) ) return dnsCache . get ( name ) ;
let out ;
try {
const r = await fetch ( ` ${ API } /api/records/ ${ encodeURIComponent ( name ) } ` , { cache : "no-store" } ) ;
if ( r . status === 404 ) out = { status : "none" , seq : 0 , dns : { } } ;
else if ( r . status === 409 ) out = { status : "nopointer" , seq : 0 , dns : { } } ;
else if ( ! r . ok ) out = { status : "error" , seq : 0 , dns : { } } ;
else { const j = await r . json ( ) ; out = { status : "ok" , seq : typeof j . seq === "number" ? j . seq : 0 , dns : j . dns || { } , raw : j } ; }
} catch { out = { status : "error" , seq : 0 , dns : { } } ; }
dnsCache . set ( name , out ) ;
return out ;
}
async function warmDnsCache ( ) {
const todo = H . names . filter ( ( n ) => ! dnsCache . has ( n . name ) ) . map ( ( n ) => n . name ) ;
let i = 0 ;
const worker = async ( ) => { while ( i < todo . length ) { const n = todo [ i ++ ] ; await fetchDns ( n ) ; updateDnsBadge ( n ) ; } } ;
await Promise . all ( [ worker ( ) , worker ( ) , worker ( ) , worker ( ) ] ) ;
renderAttention ( ) ;
}
function dnsCount ( d ) {
if ( ! d ) return 0 ;
return [ "A" , "AAAA" , "MX" , "TXT" , "NS" , "SRV" , "CAA" ] . reduce ( ( n , k ) => n + ( Array . isArray ( d [ k ] ) ? d [ k ] . length : 0 ) , 0 ) + ( d . CNAME ? 1 : 0 ) ;
}
function updateDnsBadge ( name ) {
const c = dnsCache . get ( name ) ;
document . querySelectorAll ( ` [data-dnsbadge=" ${ CSS . escape ( name ) } "] ` ) . forEach ( ( el ) => {
const n = c ? . status === "ok" ? dnsCount ( c . dns ) : 0 ;
el . className = "badge " + ( n ? "b-ok" : "b-dim" ) ;
el . textContent = n ? ` DNS · ${ n } ` : ( c ? . status === "nopointer" ? "DNS needs s3" : "no DNS" ) ;
} ) ;
if ( current . name === name ) fillDnsBadge ( ) ;
}
// ---------- classification helpers ----------
function pointsTo ( rec ) {
if ( ! rec ) return { kind : "none" , label : "not pointed" , cls : "b-warn" } ;
if ( rec . h ) return { kind : "h" , label : "site on chain" , cls : "b-x" } ;
if ( rec . s3 ) return { kind : "s3" , label : "site on Sia" , cls : "b-x" } ;
if ( rec . p ) return { kind : "p" , label : "reverse proxy" , cls : "b-x" } ;
if ( rec . ip ) return { kind : "ip" , label : "own server" , cls : "b-x" } ;
if ( rec . u ) return { kind : "u" , label : "redirect" , cls : "b-x" } ;
return { kind : "none" , label : "not pointed" , cls : "b-warn" } ;
}
2026-09-20 18:40:56 +02:00
// The public relay is navigate.st; silentmode.st is the operator host.
const siteUrl = ( name ) => ` https://navigate.st/bns/ ${ encodeURIComponent ( name ) } / ` ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
const txUrl = ( txid ) => ` https://chipnet.imaginary.cash/tx/ ${ txid } ` ;
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
// ---------- attention strip (top of Domain names) ----------
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
function renderAttention ( ) {
const items = [ ] ;
for ( const e of H . names ) {
const pt = pointsTo ( e . records ) ;
if ( pt . kind === "none" ) items . push ( { e , why : "points nowhere yet" , go : "content" } ) ;
}
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
const box = $ ( "ov-attention" ) ;
if ( ! items . length ) { box . innerHTML = "" ; return ; }
box . innerHTML = ` <div class="dim" style="margin:6px 0 4px">Needs attention</div> ` +
items . slice ( 0 , 5 ) . map ( ( { e , why , go } ) => ` <div class="drow" data-open=" ${ esc ( e . name ) } " data-sub=" ${ go } "><div class="n"> ${ nameHtml ( e ) } </div><span class="badge b-warn"> ${ esc ( why ) } </span></div> ` ) . join ( "" ) ;
}
// ---------- web-builder (Sirius Studio) ----------
function renderStudio ( ) {
const status = $ ( "studio-status" ) , list = $ ( "studio-list" ) ;
if ( ! status || ! list ) return ;
if ( ! H . names . length ) {
status . className = "status" ;
status . innerHTML = ` You need a name first. <a href="./#search-input">Register one →</a> — then come back here to build its site. ` ;
list . innerHTML = "" ; return ;
}
status . className = "status ok" ;
status . textContent = ` Pick a name to build or edit its site. ` ;
list . innerHTML = [ ... H . names ] . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) . map ( ( e ) => {
const rec = e . records || { } ;
const studioFolder = ` bns/ ${ e . name } / ` ;
const st = rec . s3 === studioFolder ? { label : "built with Studio" , cls : "b-ok" }
: rec . s3 ? { label : "site on Sia (external)" , cls : "b-x" }
: rec . h ? { label : "inline HTML" , cls : "b-x" }
: rec . u ? { label : "redirect" , cls : "b-dim" }
: rec . p || rec . ip ? { label : "own server" , cls : "b-dim" }
: { label : "no site yet" , cls : "b-warn" } ;
return ` <div class="drow" style="cursor:default">
< div class = "n" > $ { nameHtml ( e ) } < span class = "badge ${st.cls}" > $ { esc ( st . label ) } < / s p a n > < / d i v >
< div class = "acts" >
$ { rec . s3 || rec . h ? ` <a class="btn small ghost" href=" ${ siteUrl ( e . name ) } " target="_blank" rel="noopener">View →</a> ` : "" }
< a class = "btn small acid" href = "./studio.html?name=${encodeURIComponent(e.name)}" > $ { rec . s3 === studioFolder ? "Edit in Studio" : "Open in Studio" } → < / a >
< / d i v >
< / d i v > ` ;
} ) . join ( "" ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
}
// ---------- domains list ----------
function nameHtml ( e ) { return ` <span> ${ esc ( e . label ) } .<span class="tld"> ${ esc ( e . tld ) } </span></span> ` ; }
function domainRow ( e , { compact = false } = { } ) {
const pt = pointsTo ( e . records ) ;
const d = dnsCache . get ( e . name ) ;
const dn = d ? . status === "ok" ? dnsCount ( d . dns ) : 0 ;
const dnsBadge = ` <span class="badge ${ dn ? "b-ok" : "b-dim" } " data-dnsbadge=" ${ esc ( e . name ) } "> ${ dn ? ` DNS · ${ dn } ` : ( d ? . status === "nopointer" ? "DNS needs s3" : ( d ? "no DNS" : "DNS …" ) ) } </span> ` ;
const acts = compact ? "" : ` <div class="acts">
< a class = "btn small ghost" href = "${siteUrl(e.name)}" target = "_blank" rel = "noopener" data - stop > Open → < / a >
< button class = "btn small ghost" data - open = "${esc(e.name)}" data - sub = "dns" > DNS < / b u t t o n >
< button class = "btn small acid" data - open = "${esc(e.name)}" data - sub = "summary" > Manage < / b u t t o n > < / d i v > ` ;
return ` <div class="drow" data-open=" ${ esc ( e . name ) } " data-sub="summary">
< div class = "n" > $ { nameHtml ( e ) } < span class = "badge ${pt.cls}" > $ { esc ( pt . label ) } < / s p a n > $ { d n s B a d g e } < / d i v >
$ { acts }
$ { compact ? "" : ` <div class="meta"><span>block ${ e . height ? fmtInt ( e . height ) : "mempool" } </span><span>·</span><span class="mono" style="background:none;border:0;padding:0"> ${ esc ( ( e . category || "" ) . slice ( 0 , 12 ) ) } …</span></div> ` }
< / d i v > ` ;
}
function renderDomains ( ) {
const list = $ ( "names-list" ) , status = $ ( "names-status" ) ;
const q = ( $ ( "dom-search" ) . value || "" ) . trim ( ) . toLowerCase ( ) ;
const sort = $ ( "dom-sort" ) . value ;
const tldSel = $ ( "dom-tld" ) ;
// TLD filter options (rebuild keeping selection)
const tlds = [ ... new Set ( H . names . map ( ( n ) => n . tld ) ) ] . sort ( ) ;
const keep = tldSel . value ;
tldSel . innerHTML = ` <option value="">All TLDs</option> ` + tlds . map ( ( t ) => ` <option value=" ${ esc ( t ) } " ${ t === keep ? " selected" : "" } >. ${ esc ( t ) } </option> ` ) . join ( "" ) ;
let rows = H . names . filter ( ( n ) => ( ! q || n . name . includes ( q ) ) && ( ! tldSel . value || n . tld === tldSel . value ) ) ;
rows . sort ( ( a , b ) => sort === "tld" ? ( a . tld . localeCompare ( b . tld ) || a . label . localeCompare ( b . label ) )
: sort === "height" ? ( ( b . height || 1e12 ) - ( a . height || 1e12 ) )
: a . name . localeCompare ( b . name ) ) ;
$ ( "dom-count" ) . textContent = ` ${ rows . length } of ${ H . names . length } ` ;
if ( ! H . names . length ) {
status . className = "status" ; status . textContent = "No BCNR names found for this wallet. If you just registered, wait a minute for confirmation and refresh." ;
list . innerHTML = "" ; return ;
}
status . className = "status ok" ; status . textContent = ` You own ${ H . names . length } name ${ H . names . length === 1 ? "" : "s" } . ` ;
list . innerHTML = rows . length ? rows . map ( ( e ) => domainRow ( e ) ) . join ( "" ) : ` <div class="empty">Nothing matches that filter.</div> ` ;
}
[ "dom-search" , "dom-sort" , "dom-tld" ] . forEach ( ( id ) => $ ( id ) . addEventListener ( "input" , renderDomains ) ) ;
document . addEventListener ( "click" , ( e ) => {
if ( e . target . closest ( "[data-stop]" ) ) return ;
const el = e . target . closest ( "[data-open]" ) ;
if ( ! el ) return ;
e . preventDefault ( ) ;
pendingSub = el . dataset . sub || "summary" ;
go ( "domain" , el . dataset . open ) ;
} ) ;
let pendingSub = "summary" ;
// ---------- domain detail ----------
function setSub ( name ) {
document . querySelectorAll ( ".subtabs [data-sub]" ) . forEach ( ( b ) => b . classList . toggle ( "active" , b . dataset . sub === name ) ) ;
document . querySelectorAll ( '.pane[data-pane="domain"] .sub[data-sub]' ) . forEach ( ( s ) => { s . hidden = s . dataset . sub !== name ; } ) ;
if ( name === "export" ) renderExportPreview ( ) ;
}
document . querySelectorAll ( ".subtabs [data-sub]" ) . forEach ( ( b ) => b . addEventListener ( "click" , ( ) => setSub ( b . dataset . sub ) ) ) ;
async function openDomain ( name ) {
const entry = H . names . find ( ( n ) => n . name === name ) ;
if ( ! entry ) {
if ( ! H . names . length && Date . now ( ) - H . loadedAt > 1000 ) { $ ( "dd-name" ) . textContent = name ; $ ( "dd-crumb" ) . textContent = name ; return ; }
$ ( "dd-name" ) . textContent = name ; $ ( "dd-crumb" ) . textContent = name ;
$ ( "dd-kv" ) . innerHTML = ` <div class="k">Status</div><div class="v">This wallet does not hold ${ esc ( name ) } .</div> ` ;
return ;
}
current = { name , entry , manifestSeq : 0 , rows : [ ] , dirty : false } ;
$ ( "dd-name" ) . textContent = name ; $ ( "dd-crumb" ) . textContent = name ;
$ ( "dd-view" ) . href = siteUrl ( name ) ;
fillSummary ( entry ) ;
fillContentForm ( entry . records || { } ) ;
$ ( "rd-url" ) . value = entry . records ? . u || "" ;
$ ( "tr-addr" ) . value = "" ; $ ( "tr-ack" ) . checked = false ; $ ( "tr-send" ) . disabled = true ; setMsg ( "tr-msg" , "" ) ;
setMsg ( "dns-msg" , "" ) ; setMsg ( "ct-msg" , "" ) ; setMsg ( "rd-msg" , "" ) ;
setSub ( pendingSub || "summary" ) ; pendingSub = "summary" ;
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
$ ( "sell-log" ) . className = "steps-log" ; $ ( "sell-log" ) . innerHTML = "" ; setMsg ( "sell-msg" , "" ) ;
loadListing ( name ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
await loadDns ( name , true ) ;
}
function setMsg ( id , text , cls = "" ) { const m = $ ( id ) ; if ( ! m ) return ; m . textContent = text ; m . className = "msg " + cls ; }
function logger ( id ) {
const log = $ ( id ) ; log . className = "steps-log on" ; log . innerHTML = "" ;
return ( t ) => { const d = document . createElement ( "div" ) ; d . textContent = String ( t ) ; log . appendChild ( d ) ; log . scrollTop = log . scrollHeight ; } ;
}
function fillSummary ( e ) {
const kv = [
[ "Name" , ` <code> ${ esc ( e . name ) } </code> ` ] ,
[ "TLD" , ` . ${ esc ( e . tld ) } ` ] ,
[ "Owner" , ` <code> ${ esc ( wallet . address ) } </code> ` ] ,
[ "Certificate" , ` <code> ${ esc ( e . category || "" ) } </code> ` ] ,
[ "Registered" , e . height ? ` block ${ fmtInt ( e . height ) } · <a href=" ${ txUrl ( e . txid ) } " target="_blank" rel="noopener">tx →</a> ` : ` mempool · <a href=" ${ txUrl ( e . txid ) } " target="_blank" rel="noopener">tx →</a> ` ] ,
[ "Last update" , e . updated _txid ? ` <a href=" ${ txUrl ( e . updated _txid ) } " target="_blank" rel="noopener"> ${ esc ( e . updated _txid . slice ( 0 , 16 ) ) } … →</a> ` : "none since registration" ] ,
[ "Renewal" , "never — names do not expire" ] ,
2026-09-20 18:40:56 +02:00
[ "Public URL" , ` <a href=" ${ siteUrl ( e . name ) } " target="_blank" rel="noopener">Navigate.st gateway</a> <span class="dim">· any BCNR resolver opens https:// ${ esc ( e . name ) } / directly</span> ` ] ,
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
] ;
$ ( "dd-kv" ) . innerHTML = kv . map ( ( [ k , v ] ) => ` <div class="k"> ${ k } </div><div class="v"> ${ v } </div> ` ) . join ( "" ) ;
const rec = e . records || { } ;
const pt = pointsTo ( rec ) ;
const rows = [ [ "Serves" , ` <span class="badge ${ pt . cls } "> ${ esc ( pt . label ) } </span> ` ] ] ;
const labels = { h : "Inline HTML" , s3 : "Sia bucket" , p : "Reverse proxy" , ip : "Server IPv4" , tls : "TLS pin" , u : "Redirect" } ;
for ( const k of [ "h" , "s3" , "p" , "ip" , "tls" , "u" ] ) if ( rec [ k ] ) rows . push ( [ labels [ k ] , ` <code> ${ esc ( String ( rec [ k ] ) . slice ( 0 , 120 ) ) } </code> ` ] ) ;
if ( rows . length === 1 ) rows . push ( [ "Records" , ` none — <a href="#" data-sub-go="content">point it somewhere →</a> ` ] ) ;
$ ( "dd-records" ) . innerHTML = rows . map ( ( [ k , v ] ) => ` <div class="k"> ${ k } </div><div class="v"> ${ v } </div> ` ) . join ( "" ) ;
$ ( "dd-records" ) . querySelectorAll ( "[data-sub-go]" ) . forEach ( ( a ) => a . onclick = ( ev ) => { ev . preventDefault ( ) ; setSub ( a . dataset . subGo ) ; } ) ;
fillDnsBadge ( ) ;
}
function fillDnsBadge ( ) {
const c = dnsCache . get ( current . name ) ;
const n = c ? . status === "ok" ? dnsCount ( c . dns ) : 0 ;
const b = $ ( "dd-dnsbadge" ) ;
b . className = "badge " + ( n ? "b-ok" : "b-dim" ) ;
b . textContent = n ? ` DNS · ${ n } record ${ n === 1 ? "" : "s" } ` : ( c ? . status === "nopointer" ? "DNS needs s3" : "no DNS" ) ;
const sum = $ ( "dd-dns-summary" ) ;
if ( ! c ) { sum . textContent = "Loading…" ; return ; }
if ( c . status === "nopointer" ) { sum . innerHTML = ` This name has no <b>s3</b> record, so there is nowhere to store a DNS manifest yet. Set one under Content & hosting first. ` ; return ; }
if ( ! n ) { sum . innerHTML = ` No DNS records. <a href="#" data-sub-go="dns">Add some →</a> ` ; sum . querySelector ( "[data-sub-go]" ) . onclick = ( ev ) => { ev . preventDefault ( ) ; setSub ( "dns" ) ; } ; return ; }
const d = c . dns ;
const parts = [ ] ;
for ( const k of [ "A" , "AAAA" , "NS" , "TXT" ] ) if ( Array . isArray ( d [ k ] ) && d [ k ] . length ) parts . push ( ` <b> ${ k } </b> ${ d [ k ] . map ( ( v ) => esc ( String ( v ) . slice ( 0 , 48 ) ) ) . join ( ", " ) } ` ) ;
if ( d . CNAME ) parts . push ( ` <b>CNAME</b> ${ esc ( d . CNAME ) } ` ) ;
if ( Array . isArray ( d . MX ) && d . MX . length ) parts . push ( ` <b>MX</b> ${ d . MX . map ( ( m ) => ` ${ m . pref } ${ esc ( m . host ) } ` ) . join ( ", " ) } ` ) ;
if ( Array . isArray ( d . SRV ) && d . SRV . length ) parts . push ( ` <b>SRV</b> ${ d . SRV . length } ` ) ;
if ( Array . isArray ( d . CAA ) && d . CAA . length ) parts . push ( ` <b>CAA</b> ${ d . CAA . map ( ( x ) => ` ${ x . tag } ${ esc ( x . value ) } ` ) . join ( ", " ) } ` ) ;
sum . innerHTML = parts . join ( "<br>" ) + ` <div class="dim" style="margin-top:6px">manifest seq ${ c . seq } </div> ` ;
}
// ---------- DNS editor ----------
const DNS _HELP = {
A : [ "IPv4 address" , "1.2.3.4" ] , AAAA : [ "IPv6 address" , "2001:db8::1" ] , CNAME : [ "Target host (one CNAME per name)" , "target.example.com" ] ,
MX : [ "Mail host" , "mail.example.com" ] , TXT : [ "Text (SPF, DKIM, verification…)" , "v=spf1 include:_spf.silentmode.st -all" ] ,
NS : [ "Nameserver host" , "ns1.example.com" ] , SRV : [ "Target host" , "sip.example.com" ] , CAA : [ "CA domain or report URL" , "letsencrypt.org" ] ,
} ;
function rowsFromManifest ( d ) {
const rows = [ ] ;
for ( const t of [ "A" , "AAAA" , "TXT" , "NS" ] ) for ( const v of ( Array . isArray ( d ? . [ t ] ) ? d [ t ] : [ ] ) ) rows . push ( { type : t , value : String ( v ) } ) ;
if ( d ? . CNAME ) rows . push ( { type : "CNAME" , value : String ( d . CNAME ) } ) ;
for ( const m of ( Array . isArray ( d ? . MX ) ? d . MX : [ ] ) ) rows . push ( { type : "MX" , value : String ( m . host ) , prio : Number ( m . pref ) || 0 } ) ;
for ( const s of ( Array . isArray ( d ? . SRV ) ? d . SRV : [ ] ) ) rows . push ( { type : "SRV" , value : String ( s . target ) , prio : Number ( s . priority ) || 0 , weight : Number ( s . weight ) || 0 , port : Number ( s . port ) || 0 } ) ;
for ( const c of ( Array . isArray ( d ? . CAA ) ? d . CAA : [ ] ) ) rows . push ( { type : "CAA" , value : String ( c . value ) , tag : c . tag || "issue" , flags : Number ( c . flags ) || 0 } ) ;
return rows ;
}
function manifestFromRows ( rows ) {
const d = { A : [ ] , AAAA : [ ] , MX : [ ] , TXT : [ ] , CNAME : null , NS : [ ] } ;
const srv = [ ] , caa = [ ] ;
for ( const r of rows ) {
if ( r . type === "MX" ) d . MX . push ( { pref : r . prio || 0 , host : r . value } ) ;
else if ( r . type === "CNAME" ) d . CNAME = r . value ;
else if ( r . type === "SRV" ) srv . push ( { priority : r . prio || 0 , weight : r . weight || 0 , port : r . port || 0 , target : r . value } ) ;
else if ( r . type === "CAA" ) caa . push ( { flags : r . flags || 0 , tag : r . tag || "issue" , value : r . value } ) ;
else if ( d [ r . type ] ) d [ r . type ] . push ( r . value ) ;
}
if ( srv . length ) d . SRV = srv ;
if ( caa . length ) d . CAA = caa ;
return d ;
}
function extraOf ( r ) {
if ( r . type === "MX" ) return ` priority ${ r . prio } ` ;
if ( r . type === "SRV" ) return ` prio ${ r . prio } · weight ${ r . weight } · port ${ r . port } ` ;
if ( r . type === "CAA" ) return ` ${ r . tag } ${ r . flags ? ` · flags ${ r . flags } ` : "" } ` ;
return "" ;
}
function renderDnsTable ( ) {
const tb = $ ( "dns-table" ) . querySelector ( "tbody" ) ;
tb . innerHTML = current . rows . map ( ( r , i ) => ` <tr>
< td class = "t" > $ { esc ( r . type ) } < / t d > < t d c l a s s = " v " > $ { e s c ( r . v a l u e ) } < / t d > < t d c l a s s = " p " > $ { e s c ( e x t r a O f ( r ) ) } < / t d >
< td class = "a" > < button class = "x" data - del = "${i}" title = "Remove" > × < / b u t t o n > < / t d > < / t r > ` ) . j o i n ( " " ) ;
$ ( "dns-empty" ) . classList . toggle ( "hidden" , current . rows . length > 0 ) ;
$ ( "dns-seq" ) . textContent = ` seq ${ current . manifestSeq } ${ current . dirty ? " · unsaved" : "" } ` ;
tb . querySelectorAll ( "[data-del]" ) . forEach ( ( b ) => b . onclick = ( ) => { current . rows . splice ( Number ( b . dataset . del ) , 1 ) ; current . dirty = true ; renderDnsTable ( ) ; } ) ;
}
async function loadDns ( name , force ) {
const c = await fetchDns ( name , force ) ;
if ( current . name !== name ) return ;
current . manifestSeq = c . seq || 0 ;
current . rows = c . status === "ok" ? rowsFromManifest ( c . dns ) : [ ] ;
current . dirty = false ;
renderDnsTable ( ) ;
updateDnsBadge ( name ) ;
const pub = $ ( "dns-publish" ) ;
if ( c . status === "nopointer" ) {
setMsg ( "dns-msg" , "This name has no s3 record on chain, so the manifest has nowhere to live. Set one under Content & hosting first (any Sia bucket key works)." , "err" ) ;
pub . disabled = true ;
} else { pub . disabled = false ; }
}
function updateDnsForm ( ) {
const t = $ ( "dns-type" ) . value ;
document . querySelectorAll ( ".addrow .opt" ) . forEach ( ( o ) => { o . hidden = ! o . dataset . for . split ( " " ) . includes ( t ) ; } ) ;
const [ label , ph ] = DNS _HELP [ t ] || [ "Value" , "" ] ;
$ ( "dns-value-label" ) . textContent = label ; $ ( "dns-value" ) . placeholder = ph ;
$ ( "dns-help" ) . textContent = t === "CNAME" ? "A CNAME replaces any existing CNAME; classic DNS does not allow it next to other records on the same host." :
t === "TXT" ? "One string per record. Long DKIM keys are fine — there is no size cap in the manifest." :
t === "CAA" ? "Restricts which certificate authorities may issue for this name in classic DNS." : "" ;
}
$ ( "dns-type" ) . addEventListener ( "change" , updateDnsForm ) ; updateDnsForm ( ) ;
$ ( "dns-add" ) . addEventListener ( "click" , ( ) => {
const type = $ ( "dns-type" ) . value ;
const value = $ ( "dns-value" ) . value . trim ( ) ;
if ( ! value ) { $ ( "dns-value" ) . focus ( ) ; return ; }
if ( type === "A" && ! /^\d{1,3}(\.\d{1,3}){3}$/ . test ( value ) ) return setMsg ( "dns-msg" , "That is not an IPv4 address." , "err" ) ;
if ( type === "AAAA" && ! /^[0-9a-f:]+$/i . test ( value ) ) return setMsg ( "dns-msg" , "That is not an IPv6 address." , "err" ) ;
const row = { type , value } ;
if ( type === "MX" || type === "SRV" ) row . prio = Number ( $ ( "dns-prio" ) . value ) || 0 ;
if ( type === "SRV" ) { row . weight = Number ( $ ( "dns-weight" ) . value ) || 0 ; row . port = Number ( $ ( "dns-port" ) . value ) || 0 ; }
if ( type === "CAA" ) { row . tag = $ ( "dns-tag" ) . value ; row . flags = 0 ; }
if ( type === "CNAME" ) current . rows = current . rows . filter ( ( r ) => r . type !== "CNAME" ) ;
current . rows . push ( row ) ; current . dirty = true ;
$ ( "dns-value" ) . value = "" ; $ ( "dns-prio" ) . value = "" ; $ ( "dns-weight" ) . value = "" ; $ ( "dns-port" ) . value = "" ;
setMsg ( "dns-msg" , "" ) ; renderDnsTable ( ) ;
} ) ;
$ ( "dns-value" ) . addEventListener ( "keydown" , ( e ) => { if ( e . key === "Enter" ) { e . preventDefault ( ) ; $ ( "dns-add" ) . click ( ) ; } } ) ;
$ ( "dns-reload" ) . addEventListener ( "click" , ( ) => loadDns ( current . name , true ) ) ;
$ ( "dns-publish" ) . addEventListener ( "click" , async ( ) => {
if ( ! current . name || ! wallet ) return ;
if ( wallet . source === "wc" ) return setMsg ( "dns-msg" , "External-wallet signing (WizardConnect) for records manifests lands in the next drop — please sign in with the built-in wallet." , "err" ) ;
const push = logger ( "dns-log" ) ; const btn = $ ( "dns-publish" ) ;
btn . disabled = true ; btn . textContent = "Signing…" ; setMsg ( "dns-msg" , "" ) ;
try {
const manifest = { v : 1 , name : current . name , seq : ( current . manifestSeq || 0 ) + 1 , updated _at : new Date ( ) . toISOString ( ) , dns : manifestFromRows ( current . rows ) } ;
push ( ` composed manifest · seq ${ manifest . seq } · ${ current . rows . length } record ${ current . rows . length === 1 ? "" : "s" } ` ) ;
const signed = await BNS . signRecordsManifest ( wallet , manifest ) ;
push ( "signed with wallet key" ) ;
const r = await fetch ( ` ${ API } /api/records/ ${ encodeURIComponent ( current . name ) } ` , { method : "POST" , headers : { "content-type" : "application/json" } , body : JSON . stringify ( signed ) } ) ;
const j = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! r . ok ) throw new Error ( ` API ${ r . status } : ${ j . error || "unknown error" } ` ) ;
push ( ` published · ${ j . bytes || "?" } bytes at ${ j . sia _key } ` ) ;
current . manifestSeq = manifest . seq ; current . dirty = false ;
dnsCache . set ( current . name , { status : "ok" , seq : manifest . seq , dns : manifest . dns , raw : manifest } ) ;
renderDnsTable ( ) ; updateDnsBadge ( current . name ) ; renderAttention ( ) ;
setMsg ( "dns-msg" , ` Published manifest seq ${ manifest . seq } . Resolvers pick it up on their next fetch (about 30 s at the gateway). ` , "ok" ) ;
} catch ( e ) { setMsg ( "dns-msg" , "Failed: " + ( e . message || e ) , "err" ) ; push ( "error: " + ( e . message || e ) ) ; }
finally { btn . disabled = false ; btn . textContent = "Sign & publish →" ; }
} ) ;
// ---------- content & hosting (on-chain records) ----------
const CT _KEYS = [ "h" , "s3" , "p" , "ip" , "tls" ] ;
function fillContentForm ( rec ) { for ( const k of CT _KEYS ) $ ( "ct-" + k ) . value = rec [ k ] || "" ; updateBudget ( ) ; }
function collectContent ( ) {
const r = { } ;
for ( const k of CT _KEYS ) { const v = $ ( "ct-" + k ) . value . trim ( ) ; if ( v ) r [ k ] = v ; }
const u = ( $ ( "rd-url" ) . value || "" ) . trim ( ) ; if ( u ) r . u = u ;
return r ;
}
function updateBudget ( ) {
if ( ! current . name ) return ;
try {
const max = BNS . payloadBudget ( current . name , "UPD" ) ;
const used = new TextEncoder ( ) . encode ( JSON . stringify ( collectContent ( ) ) ) . length ;
$ ( "ct-budget-max" ) . textContent = max ; $ ( "ct-budget-used" ) . textContent = used ;
$ ( "ct-budget" ) . classList . toggle ( "over" , used > max ) ; $ ( "ct-save" ) . disabled = used > max ;
} catch { }
}
CT _KEYS . forEach ( ( k ) => $ ( "ct-" + k ) . addEventListener ( "input" , updateBudget ) ) ;
$ ( "rd-url" ) . addEventListener ( "input" , updateBudget ) ;
$ ( "ct-reset" ) . addEventListener ( "click" , ( ) => fillContentForm ( current . entry ? . records || { } ) ) ;
async function publishRecords ( records , logId , msgId , btnId , doneText ) {
if ( ! current . name || ! wallet ) return false ;
const push = logger ( logId ) ; const btn = $ ( btnId ) ; const orig = btn . textContent ;
btn . disabled = true ; btn . textContent = "Signing…" ; setMsg ( msgId , "" ) ;
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 : current . name , records , onProgress : ( s ) => push ( s ) } )
: await BNS . setRecordsWithBuiltInWallet ( el , { wallet , name : current . name , records , onProgress : ( s ) => push ( s ) } ) ;
push ( "broadcast: " + ( res . txid || res . txId || "(no txid)" ) ) ;
if ( current . entry ) { current . entry . records = records ; current . entry . updated _txid = res . txid || res . txId || current . entry . updated _txid ; }
feat(sirius-x): Sirius Studio web-builder + signed site uploads; shorter dashboard menu
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
2026-09-17 01:35:21 +02:00
fillSummary ( current . entry ) ; renderDomains ( ) ; renderAttention ( ) ; renderStudio ( ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
setMsg ( msgId , doneText , "ok" ) ;
return true ;
} catch ( e ) { setMsg ( msgId , "Failed: " + ( e . message || e ) , "err" ) ; push ( "error: " + ( e . message || e ) ) ; return false ; }
finally { btn . disabled = false ; btn . textContent = orig ; try { el ? . close ? . ( ) ; } catch { } }
}
$ ( "ct-save" ) . addEventListener ( "click" , ( ) => publishRecords ( collectContent ( ) , "ct-log" , "ct-msg" , "ct-save" , "Saved. Records refresh on the chain within a block." ) ) ;
$ ( "rd-save" ) . addEventListener ( "click" , ( ) => {
const u = $ ( "rd-url" ) . value . trim ( ) ;
if ( ! /^https?:\/\//i . test ( u ) ) return setMsg ( "rd-msg" , "Enter a full URL starting with http:// or https://" , "err" ) ;
publishRecords ( collectContent ( ) , "rd-log" , "rd-msg" , "rd-save" , "Redirect saved. Resolvers forward the name within a block." ) ;
} ) ;
$ ( "rd-clear" ) . addEventListener ( "click" , ( ) => { $ ( "rd-url" ) . value = "" ; publishRecords ( collectContent ( ) , "rd-log" , "rd-msg" , "rd-clear" , "Redirect removed." ) ; } ) ;
// ---------- transfer ----------
function validAddress ( a ) { try { BNS . addressToLockingBytecode ( a ) ; return true ; } catch { return false ; } }
function updateTransferBtn ( ) {
const a = $ ( "tr-addr" ) . value . trim ( ) ;
$ ( "tr-send" ) . disabled = ! ( $ ( "tr-ack" ) . checked && validAddress ( a ) && a !== wallet ? . address ) ;
}
$ ( "tr-addr" ) . addEventListener ( "input" , updateTransferBtn ) ;
$ ( "tr-ack" ) . addEventListener ( "change" , updateTransferBtn ) ;
$ ( "tr-send" ) . addEventListener ( "click" , async ( ) => {
if ( ! current . entry || ! wallet ) return ;
const to = $ ( "tr-addr" ) . value . trim ( ) ;
if ( wallet . source === "wc" ) return setMsg ( "tr-msg" , "Transfers from an external wallet land in the next drop — sign in with the built-in wallet." , "err" ) ;
if ( ! /^bchtest:/ . test ( to ) ) return setMsg ( "tr-msg" , "Chipnet addresses start with bchtest: — a mainnet address would lose the name." , "err" ) ;
if ( ! confirm ( ` Transfer ${ current . name } to ${ to } ? This cannot be undone. ` ) ) return ;
const push = logger ( "tr-log" ) ; const btn = $ ( "tr-send" ) ;
btn . disabled = true ; btn . textContent = "Transferring…" ; setMsg ( "tr-msg" , "" ) ;
let el = null ;
try {
el = await BNS . connect ( ) ; push ( "connected to chipnet" ) ;
const utxos = await BNS . getUtxosForAddresses ( el , wallet . watchedAddresses ) ;
const raw = BNS . normalizeName ( current . name ) ;
const commitment = [ ... new TextEncoder ( ) . encode ( raw ) ] . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
const cert = utxos . find ( ( u ) => u . token ? . category === current . entry . category && u . token ? . nft ? . commitment === commitment ) ;
if ( ! cert ) throw new Error ( "certificate UTXO not found in this wallet (already moved?)" ) ;
push ( "found certificate · building transfer" ) ;
const built = BNS . buildUpdateTx ( {
name : current . name , ownerAddress : wallet . address , ownerTokenAddress : BNS . toTokenAddress ( to ) ,
records : current . entry . records || { } , certificateUtxo : cert , utxos : utxos . filter ( ( u ) => ! u . token ) ,
addressToLockingBytecode : BNS . addressToLockingBytecode ,
} ) ;
const signed = BNS . signTransaction ( built . transaction , built . sourceOutputs , ( h ) => wallet . keyFor ( h ) ) ;
push ( "signed · broadcasting" ) ;
const txid = await BNS . broadcast ( el , signed . hex ) ;
push ( "broadcast: " + txid ) ;
setMsg ( "tr-msg" , ` Transferred. ${ current . name } now belongs to ${ to } . It disappears from this list after the next refresh. ` , "ok" ) ;
btn . textContent = "Transferred ✓" ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
setTimeout ( ( ) => { loadHoldings ( ) ; refreshBalance ( ) ; } , 4000 ) ; refreshBalance ( ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
} catch ( e ) { setMsg ( "tr-msg" , "Failed: " + ( e . message || e ) , "err" ) ; push ( "error: " + ( e . message || e ) ) ; btn . disabled = false ; btn . textContent = "Transfer name →" ; }
finally { try { el ? . close ? . ( ) ; } catch { } }
} ) ;
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
// ---------- signed API calls (owner proves control of the name's key) ----------
async function signedHeaders ( msg ) {
const ts = String ( Date . now ( ) ) ;
const digest = new Uint8Array ( await crypto . subtle . digest ( "SHA-256" , new TextEncoder ( ) . encode ( msg . replace ( "{ts}" , ts ) ) ) ) ;
const sig = wallet . signMessage ( digest ) ;
return { "x-bns-ts" : ts , "x-bns-sig" : btoa ( String . fromCharCode ( ... sig ) ) } ;
}
// ---------- sell (marketplace listing) ----------
const RATE = ( ) => window . siriusPricing ? . CHIPNET _SATS _PER _USD || 250000 ;
let activeListing = null ;
function paintSell ( ) {
const st = $ ( "sell-status" ) ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
$ ( "sell-rate" ) . textContent = ` 1 BCH ≈ ${ fmtUsd ( Math . round ( 1e8 / RATE ( ) * 100 ) / 100 ) } ${ window . siriusPricing ? . priceInfo ? " · " + window . siriusPricing . priceInfo ( ) : "" } ` ;
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
if ( activeListing ) {
st . className = "status ok" ; st . textContent = "This name is listed on the market." ;
$ ( "sell-form" ) . classList . add ( "hidden" ) ; $ ( "sell-active" ) . classList . remove ( "hidden" ) ;
const p = BigInt ( activeListing . price _sats ) ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
$ ( "sell-kv" ) . innerHTML = [ [ "Price" , ` ${ esc ( fmtBch ( p ) ) } (≈ ${ esc ( fmtUsd ( Math . round ( Number ( p ) / RATE ( ) * 100 ) / 100 ) ) } ) ` ] , [ "Listed" , esc ( new Date ( activeListing . created _at ) . toLocaleString ( ) ) ] , [ "Offer" , ` <code> ${ esc ( activeListing . partial _tx . slice ( 0 , 40 ) ) } …</code> ` ] , [ "Market page" , ` <a href="./market/" target="_blank" rel="noopener">open →</a> ` ] ]
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
. map ( ( [ k , v ] ) => ` <div class="k"> ${ k } </div><div class="v"> ${ v } </div> ` ) . join ( "" ) ;
} else {
st . className = "status" ; st . textContent = "Not for sale. Set a price to list it." ;
$ ( "sell-form" ) . classList . remove ( "hidden" ) ; $ ( "sell-active" ) . classList . add ( "hidden" ) ;
}
}
async function loadListing ( name ) {
activeListing = null ;
try { const r = await fetch ( ` ${ API } /api/market/ ${ encodeURIComponent ( name ) } ` , { cache : "no-store" } ) ; if ( r . ok ) activeListing = await r . json ( ) ; } catch { }
if ( current . name === name ) paintSell ( ) ;
}
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
const bchStr = ( sats ) => ( Number ( sats ) / 1e8 ) . toFixed ( 8 ) . replace ( /0+$/ , "" ) . replace ( /\.$/ , "" ) ;
$ ( "sell-usd" ) . addEventListener ( "input" , ( ) => { const v = Number ( String ( $ ( "sell-usd" ) . value ) . replace ( /[^\d.]/g , "" ) ) ; $ ( "sell-bch" ) . value = v > 0 ? bchStr ( Math . round ( v * RATE ( ) ) ) : "" ; } ) ;
$ ( "sell-bch" ) . addEventListener ( "input" , ( ) => { const b = Number ( String ( $ ( "sell-bch" ) . value ) . replace ( /[^\d.]/g , "" ) ) ; $ ( "sell-usd" ) . value = b > 0 ? String ( Math . round ( b * 1e8 / RATE ( ) * 100 ) / 100 ) : "" ; } ) ;
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
$ ( "sell-list" ) . addEventListener ( "click" , async ( ) => {
if ( ! current . entry || ! wallet ) return ;
if ( wallet . source === "wc" ) return setMsg ( "sell-msg" , "Listing from an external wallet lands in the next drop — sign in with the built-in wallet." , "err" ) ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
const sats = BigInt ( Math . round ( ( Number ( String ( $ ( "sell-bch" ) . value ) . replace ( /[^\d.]/g , "" ) ) || 0 ) * 1e8 ) ) ;
if ( sats < 10000 n ) return setMsg ( "sell-msg" , "Set a price of at least 0.0001 BCH." , "err" ) ;
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
const push = logger ( "sell-log" ) ; const btn = $ ( "sell-list" ) ; btn . disabled = true ; btn . textContent = "Signing…" ; setMsg ( "sell-msg" , "" ) ;
let el = null ;
try {
el = await BNS . connect ( ) ; push ( "connected to chipnet" ) ;
const utxos = await BNS . getUtxosForAddresses ( el , wallet . watchedAddresses ) ;
const raw = BNS . normalizeName ( current . name ) ;
const commitment = [ ... new TextEncoder ( ) . encode ( raw ) ] . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
const cert = utxos . find ( ( u ) => u . token ? . category === current . entry . category && u . token ? . nft ? . commitment === commitment ) ;
if ( ! cert ) throw new Error ( "certificate UTXO not found in this wallet" ) ;
push ( "found certificate · signing the offer (SINGLE|ANYONECANPAY)" ) ;
const listing = BNS . buildListing ( { name : current . name , certificateUtxo : cert , priceSats : sats , sellerAddress : cert . address , addressToLockingBytecode : BNS . addressToLockingBytecode , keyFor : ( h ) => wallet . keyFor ( h ) } ) ;
const r = await fetch ( ` ${ API } /api/market ` , { method : "POST" , headers : { "content-type" : "application/json" } , body : JSON . stringify ( listing ) } ) ;
const j = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! r . ok ) throw new Error ( j . error || ` API ${ r . status } ` ) ;
push ( "listed on the market" ) ;
activeListing = j . listing ; paintSell ( ) ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
setMsg ( "sell-msg" , ` Listed for ${ fmtBch ( sats ) } . Nothing moves until a buyer pays that price. ` , "ok" ) ;
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
} catch ( e ) { setMsg ( "sell-msg" , "Failed: " + ( e . message || e ) , "err" ) ; push ( "error: " + ( e . message || e ) ) ; }
finally { btn . disabled = false ; btn . textContent = "List for sale →" ; try { el ? . close ? . ( ) ; } catch { } }
} ) ;
$ ( "sell-cancel" ) . addEventListener ( "click" , async ( ) => {
if ( ! current . entry || ! wallet || ! activeListing ) return ;
if ( ! confirm ( ` Cancel the listing for ${ current . name } ? This also moves the certificate once so the signed offer becomes void. ` ) ) return ;
const push = logger ( "sell-log" ) ; const btn = $ ( "sell-cancel" ) ; btn . disabled = true ; btn . textContent = "Cancelling…" ; setMsg ( "sell-msg" , "" ) ;
try {
const h = await signedHeaders ( ` BNS-MARKET1 \n ${ current . name } \n {ts} ` ) ;
const r = await fetch ( ` ${ API } /api/market/ ${ encodeURIComponent ( current . name ) } ` , { method : "DELETE" , headers : h } ) ;
const j = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! r . ok ) throw new Error ( j . error || ` API ${ r . status } ` ) ;
push ( "removed from the market" ) ;
activeListing = null ; paintSell ( ) ;
push ( "invalidating the signed offer on chain (no-op update)…" ) ;
const ok = await publishRecords ( current . entry . records || { } , "sell-log" , "sell-msg" , "sell-cancel" , "Listing cancelled and the offer voided on chain." ) ;
if ( ! ok ) setMsg ( "sell-msg" , "Removed from the market, but the on-chain invalidation failed — retry from Content & hosting (Save records) so the old offer cannot be used." , "err" ) ;
} catch ( e ) { setMsg ( "sell-msg" , "Failed: " + ( e . message || e ) , "err" ) ; push ( "error: " + ( e . message || e ) ) ; }
finally { btn . disabled = false ; btn . textContent = "Cancel listing →" ; }
} ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
// ---------- export ----------
function zoneText ( ) {
const e = current . entry ; if ( ! e ) return "" ;
const c = dnsCache . get ( e . name ) ; const d = c ? . status === "ok" ? c . dns : { } ;
const L = [ ` ; ${ e . name } — exported from Sirius.X ${ new Date ( ) . toISOString ( ) } ` , ` $ ORIGIN ${ e . name } . ` , ` $ TTL 300 ` , ` ` ] ;
const rec = e . records || { } ;
L . push ( ` ; on-chain records (authoritative for BCNR resolvers) ` ) ;
for ( const k of [ "h" , "s3" , "p" , "ip" , "tls" , "u" ] ) if ( rec [ k ] ) L . push ( ` ; ${ k } = ${ String ( rec [ k ] ) . replace ( /\n/g , " " ) } ` ) ;
if ( rec . ip && ! ( Array . isArray ( d . A ) && d . A . length ) ) L . push ( ` @ \t IN \t A \t ${ rec . ip } ` ) ;
L . push ( ` ` , ` ; signed DNS manifest ${ c ? . seq ? ` (seq ${ c . seq } ) ` : "" } ` ) ;
for ( const v of d . A || [ ] ) L . push ( ` @ \t IN \t A \t ${ v } ` ) ;
for ( const v of d . AAAA || [ ] ) L . push ( ` @ \t IN \t AAAA \t ${ v } ` ) ;
if ( d . CNAME ) L . push ( ` @ \t IN \t CNAME \t ${ d . CNAME } . ` ) ;
for ( const m of d . MX || [ ] ) L . push ( ` @ \t IN \t MX \t ${ m . pref } \t ${ m . host } . ` ) ;
for ( const v of d . NS || [ ] ) L . push ( ` @ \t IN \t NS \t ${ v } . ` ) ;
for ( const v of d . TXT || [ ] ) L . push ( ` @ \t IN \t TXT \t " ${ String ( v ) . replace ( /"/g , '\\"' ) } " ` ) ;
for ( const s of d . SRV || [ ] ) L . push ( ` @ \t IN \t SRV \t ${ s . priority } ${ s . weight } ${ s . port } \t ${ s . target } . ` ) ;
for ( const x of d . CAA || [ ] ) L . push ( ` @ \t IN \t CAA \t ${ x . flags || 0 } ${ x . tag } " ${ x . value } " ` ) ;
return L . join ( "\n" ) + "\n" ;
}
function renderExportPreview ( ) { $ ( "ex-preview" ) . textContent = zoneText ( ) ; }
function download ( filename , text , type = "text/plain" ) {
const a = document . createElement ( "a" ) ;
a . href = URL . createObjectURL ( new Blob ( [ text ] , { type } ) ) ; a . download = filename ; a . click ( ) ;
setTimeout ( ( ) => URL . revokeObjectURL ( a . href ) , 1000 ) ;
}
$ ( "ex-zone" ) . addEventListener ( "click" , ( ) => download ( ` ${ current . name } .zone.txt ` , zoneText ( ) ) ) ;
$ ( "ex-json" ) . addEventListener ( "click" , ( ) => {
const c = dnsCache . get ( current . name ) ;
download ( ` ${ current . name } .json ` , JSON . stringify ( { name : current . name , chain : current . entry , dns : c ? . raw || null , exported _at : new Date ( ) . toISOString ( ) } , null , 2 ) , "application/json" ) ;
} ) ;
$ ( "ex-copy" ) . addEventListener ( "click" , ( ) => { const c = dnsCache . get ( current . name ) ; navigator . clipboard ? . writeText ( JSON . stringify ( c ? . raw || { dns : { } } , null , 2 ) ) ; } ) ;
// ---------- TLDs ----------
function isTldHidden ( records ) { const h = records ? . hidden ; return h === 1 || h === true || h === "1" ; }
function tldPriceOf ( records ) { const p = records ? . price ; return typeof p === "number" && Number . isFinite ( p ) && p >= 0 ? p : null ; }
const fmtUsd = ( n ) => ( window . siriusPricing ? . formatUsd || ( ( x ) => "$" + x ) ) ( n ) ;
function renderTlds ( ) {
const status = $ ( "tlds-status" ) , list = $ ( "tlds-list" ) ;
if ( ! H . tlds . length ) {
status . className = "status" ;
status . innerHTML = ` This wallet does not hold any TLD certificates yet. <a href="#newtld" data-go="newtld">Register one →</a> — every name registered under it earns you 90% of the service fee. ` ;
list . innerHTML = "" ; return ;
}
status . className = "status ok" ; status . textContent = ` You own ${ H . tlds . length } TLD ${ H . tlds . length === 1 ? "" : "s" } . ` ;
list . innerHTML = H . tlds . map ( ( t ) => {
const rec = t . records || { } ; const off = isTldHidden ( rec ) ; const price = tldPriceOf ( rec ) ;
const under = H . names . filter ( ( n ) => n . tld === t . tld ) . length ;
return ` <div class="drow" data-go="tld" data-arg=" ${ esc ( t . tld ) } ">
< div class = "n" > . < span class = "tld" > $ { esc ( t . tld ) } < / s p a n >
< span class = "badge ${off ? " b - off " : " b - ok "}" > $ { off ? "off · private" : "on · public" } < / s p a n >
< span class = "badge b-x" > $ { price != null ? ` ${ esc ( fmtUsd ( price ) ) } / name ` : "tier pricing" } < / s p a n > < / d i v >
< div class = "acts" > < button class = "btn small acid" data - go = "tld" data - arg = "${esc(t.tld)}" > Manage < / b u t t o n > < / d i v >
< div class = "meta" > < span > $ { under } of your names under it < / s p a n > < s p a n > · < / s p a n > < s p a n > p o l i c y $ { e s c ( r e c . p o l i c y | | " o p e n " ) } < / s p a n > < / d i v >
< / d i v > ` ;
} ) . join ( "" ) ;
}
async function registry ( ) {
if ( Date . now ( ) - registryCache . at < 60000 ) return registryCache . names ;
try {
const r = await fetch ( ` ${ API } /api/registry ` , { cache : "no-store" } ) ;
const j = await r . json ( ) ; registryCache = { at : Date . now ( ) , names : j . names || [ ] } ;
} catch { registryCache . at = Date . now ( ) ; }
return registryCache . names ;
}
async function openTld ( label ) {
const t = H . tlds . find ( ( x ) => x . tld === label ) ;
currentTld = t || null ;
$ ( "td-name" ) . textContent = "." + label ; $ ( "td-crumb" ) . textContent = "." + label ; $ ( "td-suffix" ) . textContent = "." + label ;
setMsg ( "td-msg" , "" ) ;
if ( ! t ) { $ ( "td-kv" ) . innerHTML = ` <div class="k">Status</div><div class="v">This wallet does not hold . ${ esc ( label ) } .</div> ` ; return ; }
const rec = t . records || { } ; const off = isTldHidden ( rec ) ; const price = tldPriceOf ( rec ) ;
const st = $ ( "td-state" ) ; st . className = "badge " + ( off ? "b-off" : "b-ok" ) ; st . textContent = off ? "off · private" : "on · public" ;
$ ( "td-price" ) . textContent = price != null ? ` ${ fmtUsd ( price ) } / name ` : "tier pricing" ;
$ ( "td-toggle" ) . textContent = off ? "Switch on" : "Switch off" ;
let pub = null ;
try { const j = await ( await fetch ( TLD _API , { cache : "no-store" } ) ) . json ( ) ; pub = ( j . tlds || [ ] ) . find ( ( x ) => x . tld === label ) || null ; } catch { }
const kv = [
[ "Certificate" , ` <code> ${ esc ( t . category ) } </code> ` ] ,
[ "Owner" , ` <code> ${ esc ( t . owner || wallet . address ) } </code> ` ] ,
[ "Registered" , t . reg _height ? ` block ${ fmtInt ( t . reg _height ) } ` : "mempool" ] ,
[ "Last policy update" , t . updated _txid ? ` <a href=" ${ txUrl ( t . updated _txid ) } " target="_blank" rel="noopener"> ${ esc ( t . updated _txid . slice ( 0 , 16 ) ) } … →</a> ` : "none" ] ,
[ "Names registered" , pub ? String ( pub . registered _count ) : "—" ] ,
[ "Price" , price != null ? ` ${ esc ( fmtUsd ( price ) ) } per name (you receive 90%) ` : "length tiers ($0.10 – $5)" ] ,
[ "Listing" , off ? "off — private: only you can register names" : "on — anyone can register names" ] ,
[ "Policy" , esc ( rec . policy || "open" ) + ( rec . min _len ? ` · min length ${ rec . min _len } ` : "" ) + ( rec . reserved ? ` · reserved: ${ esc ( rec . reserved ) } ` : "" ) ] ,
] ;
$ ( "td-kv" ) . innerHTML = kv . map ( ( [ k , v ] ) => ` <div class="k"> ${ k } </div><div class="v"> ${ v } </div> ` ) . join ( "" ) ;
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
loadCosign ( label ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
const names = ( await registry ( ) ) . filter ( ( n ) => n . tld === label ) ;
$ ( "td-names" ) . innerHTML = names . length
? names . map ( ( n ) => ` <div class="drow" style="cursor:default"><div class="n"><span> ${ esc ( n . name ) } </span> ${ H . names . some ( ( m ) => m . name === n . name ) ? '<span class="badge b-x">yours</span>' : "" } </div>
< div class = "acts" > < a class = "btn small ghost" href = "${siteUrl(n.name)}" target = "_blank" rel = "noopener" > Open → < / a > < / d i v >
< div class = "meta" > < span > block $ { n . height ? fmtInt ( n . height ) : "mempool" } < / s p a n > < / d i v > < / d i v > ` ) . j o i n ( " " )
: ` <div class="empty">No names registered under . ${ esc ( label ) } yet.</div> ` ;
}
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
// ---------- co-sign queue (registrations under this TLD awaiting the owner) ----------
async function loadCosign ( label ) {
const box = $ ( "td-cosign" ) ;
box . innerHTML = ` <div class="empty">Checking…</div> ` ;
let list = [ ] ;
try { const j = await ( await fetch ( ` ${ API } /api/cosign?tld= ${ encodeURIComponent ( label ) } ` , { cache : "no-store" } ) ) . json ( ) ; list = j . requests || [ ] ; } catch { }
if ( currentTld ? . tld !== label ) return ;
box . innerHTML = list . length
? list . map ( ( r ) => ` <div class="drow" style="cursor:default"><div class="n"><span> ${ esc ( r . name ) } </span><span class="badge b-warn">awaiting you</span></div>
< div class = "acts" > < button class = "btn small acid" data - approve = "${esc(r.id)}" > Approve & amp ; broadcast < / b u t t o n > < b u t t o n c l a s s = " b t n s m a l l d a n g e r " d a t a - d e c l i n e = " $ { e s c ( r . i d ) } " > D e c l i n e < / b u t t o n > < / d i v >
< div class = "meta" > < span > buyer $ { esc ( r . buyer . replace ( /^[^:]+:/ , "" ) . slice ( 0 , 10 ) ) } … < / s p a n > < s p a n > · < / s p a n > < s p a n > $ { e s c ( n e w D a t e ( r . c r e a t e d _ a t ) . t o L o c a l e S t r i n g ( ) ) } < / s p a n > $ { r . n o t e ? ` < s p a n > · < / s p a n > < s p a n > “ $ { e s c ( r . n o t e ) } ” < / s p a n > ` : " " } < / d i v > < / d i v > ` ) . j o i n ( " " )
: ` <div class="empty">No requests waiting.</div> ` ;
}
$ ( "td-cosign" ) . addEventListener ( "click" , async ( e ) => {
const a = e . target . closest ( "[data-approve]" ) , d = e . target . closest ( "[data-decline]" ) ;
if ( ! a && ! d ) return ;
if ( ! currentTld || ! wallet ) return ;
if ( wallet . source === "wc" ) return setMsg ( "td-cosign-msg" , "Co-signing from an external wallet lands in the next drop — sign in with the built-in wallet." , "err" ) ;
const id = ( a || d ) . dataset . approve || ( a || d ) . dataset . decline ;
const btn = a || d ; btn . disabled = true ; setMsg ( "td-cosign-msg" , "" ) ;
let el = null ;
try {
const h = await signedHeaders ( ` BNS-COSIGN1
$ { id }
{ ts } ` );
if ( a ) {
const r = await fetch ( ` ${ API } /api/cosign/ ${ encodeURIComponent ( id ) } ` , { cache : "no-store" } ) ;
if ( ! r . ok ) throw new Error ( "request is gone (withdrawn or expired)" ) ;
const req = await r . json ( ) ;
const signed = BNS . signCosignRequest ( req , ( lock ) => wallet . keyFor ( lock ) ) ;
if ( ! signed . signedInputs ) throw new Error ( "this wallet does not hold the certificate input of that transaction" ) ;
el = await BNS . connect ( ) ;
const txid = await BNS . broadcast ( el , signed . hex ) ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
setTimeout ( refreshBalance , 1500 ) ;
feat(bns): name marketplace and TLD-owner co-sign rule
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
2026-09-17 04:04:22 +02:00
setMsg ( "td-cosign-msg" , ` Approved — ${ req . name } registered in ${ txid . slice ( 0 , 16 ) } …. Your TLD certificate came back to you in the same transaction. ` , "ok" ) ;
}
const del = await fetch ( ` ${ API } /api/cosign/ ${ encodeURIComponent ( id ) } ` , { method : "DELETE" , headers : h } ) ;
if ( ! del . ok && d ) throw new Error ( ` API ${ del . status } ` ) ;
if ( d ) setMsg ( "td-cosign-msg" , "Declined — the request was removed. The buyer's coins never left their wallet." , "ok" ) ;
await loadCosign ( currentTld . tld ) ;
if ( a ) { registryCache = { at : 0 , names : [ ] } ; openTld ( currentTld . tld ) ; }
} catch ( err ) { setMsg ( "td-cosign-msg" , "Failed: " + ( err . message || err ) , "err" ) ; btn . disabled = false ; }
finally { try { el ? . close ? . ( ) ; } catch { } }
} ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
$ ( "td-register" ) . addEventListener ( "click" , ( ) => {
if ( ! currentTld ) return ;
const input = $ ( "td-label" ) ; const label = String ( input . value || "" ) . toLowerCase ( ) . replace ( /[^a-z0-9-]/g , "" ) ;
input . value = label ; if ( ! label ) return input . focus ( ) ;
if ( typeof window . siriusRegisterName === "function" ) window . siriusRegisterName ( ` ${ label } . ${ currentTld . tld } ` ) ;
} ) ;
$ ( "td-label" ) . addEventListener ( "keydown" , ( e ) => { if ( e . key === "Enter" ) $ ( "td-register" ) . click ( ) ; } ) ;
$ ( "td-toggle" ) . addEventListener ( "click" , async ( ) => {
if ( ! currentTld || ! wallet ) return ;
if ( wallet . source === "wc" ) return setMsg ( "td-msg" , "External-wallet (WizardConnect) TUPD lands in the next drop — please sign in with the built-in wallet." , "err" ) ;
const rec = { ... ( currentTld . records || { } ) } ; const turningOff = ! isTldHidden ( rec ) ;
if ( turningOff ) rec . hidden = 1 ; else delete rec . hidden ;
const btn = $ ( "td-toggle" ) ; btn . disabled = true ; btn . textContent = "Signing…" ;
let el = null ;
try {
el = await BNS . connect ( ) ;
const res = await BNS . setTldRecordsWithBuiltInWallet ( el , { wallet , tld : currentTld . tld , records : rec , category : currentTld . category , onProgress : ( ) => { } } ) ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
currentTld . records = rec ; setTimeout ( refreshBalance , 1500 ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
setMsg ( "td-msg" , ` ${ turningOff ? "Off" : "On" } — broadcast ${ ( res . txid || "" ) . slice ( 0 , 16 ) } …. The public registry picks it up within a minute. ` , "ok" ) ;
openTld ( currentTld . tld ) ; renderTlds ( ) ;
} catch ( e ) { setMsg ( "td-msg" , "Failed: " + ( e . message || e ) , "err" ) ; }
finally { btn . disabled = false ; try { el ? . close ? . ( ) ; } catch { } }
} ) ;
$ ( "td-policy" ) . addEventListener ( "click" , ( ) => { if ( currentTld ) openTldEditor ( currentTld . tld , currentTld . category , currentTld . records || { } ) ; } ) ;
// ---------- TLD policy editor (TUPD) ----------
const TLD _POLICY _FIELDS = [
{ key : "price" , id : "tld-ed-price" , parse : parseUsdOrNull } ,
{ key : "hidden" , id : "tld-ed-hidden" , parse : ( v ) => ( String ( v ) === "1" ? 1 : null ) } ,
{ key : "min_len" , id : "tld-ed-minlen" , parse : parseIntOrNull } ,
{ key : "fee_bps" , id : "tld-ed-feebps" , parse : parseIntOrNull } ,
{ key : "policy" , id : "tld-ed-policy" , parse : trimOrNull } ,
{ key : "reserved" , id : "tld-ed-reserved" , parse : trimOrNull } ,
{ key : "renewal_period" , id : "tld-ed-renewal" , parse : parseIntOrNull } ,
{ key : "renewal_fee" , id : "tld-ed-renewalfee" , parse : parseIntOrNull } ,
{ key : "site" , id : "tld-ed-site" , parse : trimOrNull } ,
] ;
function parseIntOrNull ( v ) { v = String ( v ) . trim ( ) ; if ( ! v ) return null ; const n = Number ( v ) ; return Number . isFinite ( n ) && n >= 0 ? Math . floor ( n ) : null ; }
function parseUsdOrNull ( v ) { v = String ( v ) . trim ( ) . replace ( /^\$/ , "" ) ; if ( ! v ) return null ; const n = Number ( v ) ; return Number . isFinite ( n ) && n >= 0 ? Math . round ( n * 100 ) / 100 : null ; }
function trimOrNull ( v ) { v = String ( v ) . trim ( ) ; return v || null ; }
let tldEditor = { tld : null , category : null } ;
function openTldEditor ( tld , category , records ) {
tldEditor = { tld , category } ;
$ ( "tld-ed-name" ) . textContent = "." + tld ;
for ( const f of TLD _POLICY _FIELDS ) { const cur = records ? . [ f . key ] ; $ ( f . id ) . value = cur == null ? "" : String ( cur ) ; }
setMsg ( "tld-ed-msg" , "" ) ; $ ( "tld-ed-log" ) . className = "steps-log" ; $ ( "tld-ed-log" ) . innerHTML = "" ;
$ ( "tld-ed-save" ) . disabled = false ; $ ( "tld-ed-save" ) . textContent = "Save policy →" ;
updateTldBudget ( ) ; $ ( "tld-editor" ) . classList . add ( "open" ) ;
}
function closeTldEditor ( ) { $ ( "tld-editor" ) . classList . remove ( "open" ) ; tldEditor = { tld : null , category : null } ; }
$ ( "tld-ed-close" ) . addEventListener ( "click" , closeTldEditor ) ;
$ ( "tld-ed-cancel" ) . addEventListener ( "click" , closeTldEditor ) ;
$ ( "tld-editor" ) . addEventListener ( "click" , ( e ) => { if ( e . target === $ ( "tld-editor" ) ) closeTldEditor ( ) ; } ) ;
function collectTldRecords ( ) { const r = { } ; for ( const f of TLD _POLICY _FIELDS ) { const v = f . parse ( $ ( f . id ) . value ) ; if ( v !== null && v !== undefined && v !== "" ) r [ f . key ] = v ; } return r ; }
function updateTldBudget ( ) {
if ( ! tldEditor . tld ) return ;
try {
const max = BNS . payloadBudget ( tldEditor . tld , "TUPD" ) ;
const used = new TextEncoder ( ) . encode ( JSON . stringify ( collectTldRecords ( ) ) ) . length ;
$ ( "tld-ed-budget-max" ) . textContent = max ; $ ( "tld-ed-budget-used" ) . textContent = used ;
$ ( "tld-ed-budget" ) . classList . toggle ( "over" , used > max ) ; $ ( "tld-ed-save" ) . disabled = used > max ;
} catch { }
}
for ( const f of TLD _POLICY _FIELDS ) $ ( f . id ) . addEventListener ( "input" , updateTldBudget ) ;
$ ( "tld-ed-save" ) . addEventListener ( "click" , async ( ) => {
if ( ! tldEditor . tld || ! tldEditor . category || ! wallet ) return ;
if ( wallet . source === "wc" ) return setMsg ( "tld-ed-msg" , "External-wallet (WizardConnect) TUPD lands in the next drop — please sign in with the built-in wallet." , "err" ) ;
const records = collectTldRecords ( ) ; const push = logger ( "tld-ed-log" ) ;
$ ( "tld-ed-save" ) . disabled = true ; $ ( "tld-ed-save" ) . textContent = "Signing…" ; setMsg ( "tld-ed-msg" , "" ) ;
let el = null ;
try {
el = await BNS . connect ( ) ; push ( "connected to chipnet" ) ;
const res = await BNS . setTldRecordsWithBuiltInWallet ( el , { wallet , tld : tldEditor . tld , records , category : tldEditor . category , onProgress : ( s ) => push ( String ( s ) ) } ) ;
push ( "broadcast: " + ( res . txid || "(no txid)" ) ) ;
const t = H . tlds . find ( ( x ) => x . tld === tldEditor . tld ) ; if ( t ) t . records = records ;
setMsg ( "tld-ed-msg" , "Policy saved. It reflects in registrations under this TLD within a block." , "ok" ) ;
$ ( "tld-ed-save" ) . textContent = "Saved ✓" ;
renderTlds ( ) ; if ( currentTld ? . tld === tldEditor . tld ) openTld ( tldEditor . tld ) ;
setTimeout ( closeTldEditor , 1200 ) ;
} catch ( e ) { setMsg ( "tld-ed-msg" , "Failed: " + ( e . message || e ) , "err" ) ; push ( "error: " + ( e . message || e ) ) ; $ ( "tld-ed-save" ) . disabled = false ; $ ( "tld-ed-save" ) . textContent = "Save policy →" ; }
finally { try { el ? . close ? . ( ) ; } catch { } }
} ) ;
// ---------- new TLD ----------
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
function usdToSats ( usd ) { const n = Number ( String ( usd ) . replace ( /[^\d.]/g , "" ) ) || 0 ; return Math . max ( 0 , Math . round ( n * RATE ( ) ) ) ; }
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
function updateTldPrice ( ) {
const sats = usdToSats ( $ ( "tld-usd" ) . value . trim ( ) || "0" ) ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
$ ( "tld-price" ) . innerHTML = ` = <b> ${ esc ( fmtBits ( sats ) ) } </b> · 1 BCH ≈ <b> ${ esc ( fmtUsd ( Math . round ( 1e8 / RATE ( ) * 100 ) / 100 ) ) } </b> ${ window . siriusPricing ? . priceInfo ? " · " + esc ( window . siriusPricing . priceInfo ( ) ) : "" } ` ;
$ ( "tld-submit" ) . textContent = ` Register (~ ${ fmtBits ( sats ) } ) ` ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
}
const validateTldLabel = ( l ) => / ^ [ a - z0 - 9 - ] { 1 , 16 } $ / . test ( l ) ;
function updateTldButtons ( ) { const ok = validateTldLabel ( $ ( "tld-label" ) . value . trim ( ) . toLowerCase ( ) ) ; $ ( "tld-check" ) . disabled = ! ok || ! wallet ; $ ( "tld-submit" ) . disabled = ! ok || ! wallet ; }
$ ( "tld-label" ) . addEventListener ( "input" , ( ) => { $ ( "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 = "" ;
try {
const j = await ( await fetch ( TLD _API ) ) . json ( ) ;
const taken = ( j . tlds || [ ] ) . some ( ( t ) => ( t . tld || t ) === label ) ;
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 = "" ;
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 args = { tld : label , records : { } , serviceFee , tldListUrl : "https://navigate.st/api/tlds" , onProgress : ( s ) => push ( s ) } ;
const res = wallet . source === "wc"
? await BNS . registerTldWithExternalWallet ( el , { session : wallet . session , ... args } )
: await BNS . registerTldWithBuiltInWallet ( el , { wallet , ... args } ) ;
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 ✓" ;
feat(bns): co-sign rule in Ariadne and mobile; live zero-conf balance in the dashboard
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.
2026-09-17 08:13:05 +02:00
setTimeout ( ( ) => { loadHoldings ( ) ; refreshBalance ( ) ; } , 2500 ) ; refreshBalance ( ) ;
feat(sirius-x): dashboard with sidebar, persistent sign-in, full DNS panel
The portal was a flat page with tabs that asked for the password or PIN on
every visit and offered little beyond a records form. Owners need a
control panel they can live in.
Sign in once: the recovery phrase is kept encrypted under a
non-extractable browser key (IndexedDB) so the next visit opens the
dashboard silently; sign-out or the Settings toggle destroys it. Payments
approve with one click unless "Ask for PIN before payments" is on.
Dashboard: left menu (Overview, Domain names, My TLD list, Wallet,
Register name/TLD, Settings). Per-name detail with a summary, a DNS
record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the
manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u),
ownership transfer (UPD that re-issues the certificate to the recipient)
and zone-file/JSON export. Per-TLD detail with policy, on/off, owner
registration and the public list of names under it. Overview flags names
that point nowhere or lack DNS.
2026-09-17 01:09:20 +02:00
} catch ( e ) {
msg . style . color = "var(--taken)" ; const raw = String ( e . message || e ) ;
msg . innerHTML = /wallet is empty/i . test ( raw )
? ` 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>. `
: "Failed: " + esc ( raw ) ;
push ( "error: " + raw ) ; $ ( "tld-submit" ) . disabled = false ; updateTldPrice ( ) ;
} finally { try { el ? . close ? . ( ) ; } catch { } }
} ) ;
updateTldPrice ( ) ;
// ---------- settings ----------
function paintSettings ( ) {
const S = window . siriusSession , P = window . siriusPin ;
$ ( "set-stay" ) . checked = ! ! S ? . isEnabled ? . ( ) ;
$ ( "set-pinpay" ) . checked = ! ! S ? . requirePinForPayments ? . ( ) ;
const hasPin = ! ! P ? . hasPin ? . ( ) ;
$ ( "set-pinstate" ) . textContent = hasPin ? "A PIN is set on this device." : "No PIN set." ;
$ ( "set-clearpin" ) . disabled = ! hasPin ;
$ ( "set-setpin" ) . disabled = ! wallet ? . mnemonic ;
}
$ ( "set-stay" ) . addEventListener ( "change" , async ( e ) => {
const S = window . siriusSession ; if ( ! S ) return ;
S . setEnabled ( e . target . checked ) ;
if ( e . target . checked && wallet ? . mnemonic ) await S . remember ( wallet . mnemonic , { accountPath : wallet . accountPath } ) ;
setMsg ( "set-msg" , e . target . checked ? "This device stays signed in." : "You will be asked to unlock on your next visit." , "ok" ) ;
} ) ;
$ ( "set-pinpay" ) . addEventListener ( "change" , ( e ) => { window . siriusSession ? . setRequirePinForPayments ( e . target . checked ) ; setMsg ( "set-msg" , e . target . checked ? "Payments will ask for your PIN. Set one below if you have none." : "Payments approve with one click." , "ok" ) ; } ) ;
$ ( "set-setpin" ) . addEventListener ( "click" , ( ) => { $ ( "pin-1" ) . value = "" ; $ ( "pin-2" ) . value = "" ; setMsg ( "pin-msg" , "" ) ; $ ( "pin-modal" ) . classList . add ( "open" ) ; $ ( "pin-1" ) . focus ( ) ; } ) ;
const closePin = ( ) => $ ( "pin-modal" ) . classList . remove ( "open" ) ;
$ ( "pin-close" ) . addEventListener ( "click" , closePin ) ; $ ( "pin-cancel" ) . addEventListener ( "click" , closePin ) ;
$ ( "pin-modal" ) . addEventListener ( "click" , ( e ) => { if ( e . target === $ ( "pin-modal" ) ) closePin ( ) ; } ) ;
$ ( "pin-save" ) . addEventListener ( "click" , async ( ) => {
const a = $ ( "pin-1" ) . value , b = $ ( "pin-2" ) . value ;
if ( ! /^\d{4,6}$/ . test ( a ) ) return setMsg ( "pin-msg" , "PIN must be 4– 6 digits" , "err" ) ;
if ( a !== b ) return setMsg ( "pin-msg" , "PINs do not match" , "err" ) ;
try { await window . siriusPin . savePinBlob ( wallet . mnemonic , a ) ; closePin ( ) ; paintSettings ( ) ; setMsg ( "set-msg" , "PIN saved." , "ok" ) ; }
catch ( e ) { setMsg ( "pin-msg" , e . message || String ( e ) , "err" ) ; }
} ) ;
$ ( "set-clearpin" ) . addEventListener ( "click" , ( ) => { window . siriusPin ? . clear ? . ( ) ; paintSettings ( ) ; setMsg ( "set-msg" , "PIN removed." , "ok" ) ; } ) ;
feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
2026-09-20 18:18:43 +02:00
// Live BCH/USD rate (pricing.js) — repaint everything priced in USD.
window . addEventListener ( "sirius:price" , ( ) => { try { updateTldPrice ( ) ; } catch { } try { if ( current . name ) paintSell ( ) ; } catch { } } ) ;