2026-09-07 22:36:02 +02:00
// Reusable name-registration mint flow — the modal + wallet setup + on-chain
// mint sequence that used to live inside register.html. Any Sirius.X page can
// include this module and call `window.siriusRegisterName(fullName)` to open
// the flow for a specific `<label>.<tld>`. The landing's search cards call
// it directly; a legacy register.html redirect keeps old links working.
//
// Injects its own modal HTML into <body> and its own CSS into <head> on load
// so the host page needs nothing but a single script include.
feat(registrar): 90/10 revenue split — TLD owner earns from name mints
Every second-level name registration under a TLD now routes 90% of the
service fee to whoever holds that TLD's certificate on chain, with 10%
going to the platform address. That's the economic incentive for
minting a TLD: you earn from every name registered under it.
The mechanism, end to end:
1. resolver-web.js fetchTldMap now also records mintScriptHex — the
scriptPubKey of the TREG output that carries each TLD's NFT.
Exported so registrar can decode it into a cashaddr with libauth.
MVP: this is the ORIGINAL owner; NFT transfers after mint are not
traced yet (a follow-up will walk the chain of transfers).
2. registrar.js gains findTldOwnerAddress(client, tld) and
splitServiceFee(sats). The split constants live at the top of the
file (TLD_OWNER_SHARE_NUM/DEN = 90/100) so the ratio moves in one
place. Rounding: BigInt division favours the platform on odd sat
counts so the two shares always sum EXACTLY to the input.
3. quoteRegistration wraps the existing flow: it derives the TLD from
the name, looks up the TLD owner, and if the owner ≠ buyer it asks
buildRegistrationTx to add a second fee output. If the owner
couldn't be resolved (TLD not registered, decode failure) the full
fee stays on the platform address — the buyer still pays the same
amount either way.
4. register-tx.js buildRegistrationTx accepts tldFeeAddress/tldFeeSats
and, when set, emits an extra P2PKH output for the TLD owner. Sits
between the beacon dust and the platform-fee output; outputMap
records .tldOwnerFee so callers can find it. costs also carries
tldOwnerFeeSats and netCostSats includes it.
5. priceSummary in registrar-config splits the 'Service fee' row into
'Service fee — TLD owner (90%)' + 'Service fee — platform (10%)'
whenever tldOwnerFeeSats > 0, with a per-line note explaining
where the money goes.
Bundle: re-exported findTldOwnerAddress + splitServiceFee from
register-entry.js. Rebuilt bns-register.js (~34 kB) and deployed;
cache-buster bumped to ?v=20260908split on portal / admin /
register-flow.js.
Verified live: findTldOwnerAddress('.bch') returns the operator
cashaddr; quoteRegistration('tester42.bch') builds cleanly with a
9,000/1,000 split output pair on a 10,000-sat fee; priceSummary
renders both lines. No regressions on the TLD-mint flow (buyer IS
the TLD owner there — split short-circuits and it stays a single
fee output as before).
2026-09-08 02:42:16 +02:00
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260908split" ;
2026-09-07 22:36:02 +02:00
const $ = ( id ) => document . getElementById ( id ) ;
const esc = ( s ) => String ( s ) . replace ( /[&<>"']/g , ( c ) => ( { "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ , "'" : "'" } [ c ] ) ) ;
const sats = ( v ) => Number ( v ) . toLocaleString ( "en-US" ) ;
// ---------- inject CSS + modal HTML ----------
// The style block is a straight copy of register.html's modal/sheet styling
// — kept scoped by prefixing every selector with #sirius-register-modal so it
// cannot collide with the host page's own .modal / .sheet classes.
( function injectAssets ( ) {
if ( document . getElementById ( "sirius-register-flow-css" ) ) return ;
const css = document . createElement ( "style" ) ;
css . id = "sirius-register-flow-css" ;
css . textContent = `
# sirius - register - modal { position : fixed ; inset : 0 ; background : rgba ( 6 , 9 , 14 , . 82 ) ; backdrop - filter : blur ( 3 px ) ;
display : none ; align - items : flex - start ; justify - content : center ; padding : 3 vh 1 rem ; overflow - y : auto ; z - index : 100 }
# sirius - register - modal . open { display : flex }
# sirius - register - modal . sheet { background : var ( -- panel , # 141 a24 ) ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - radius : 16 px ; max - width : 620 px ; width : 100 % ; padding : 26 px 28 px 28 px ; color : var ( -- ink , # e7eaf1 ) }
# sirius - register - modal . sheet h3 { margin : 0 0 . 2 rem ; font - size : 1.25 rem }
# sirius - register - modal . sheet . sub { color : var ( -- mut , # 8 b98a9 ) ; font - size : 14 px ; margin : 0 0 1.2 rem }
# sirius - register - modal . sheet label { display : block ; font - size : 13 px ; color : var ( -- mut , # 8 b98a9 ) ; margin : 14 px 0 5 px }
# sirius - register - modal . sheet input [ type = text ] ,
# sirius - register - modal . sheet input [ type = password ] ,
# sirius - register - modal . sheet textarea ,
# sirius - register - modal . sheet select {
width : 100 % ; padding : 11 px 13 px ; border - radius : 10 px ; border : 1 px solid # ffffff22 ; background : # 0e131 b ;
color : var ( -- ink , # e7eaf1 ) ; font - size : 14 px ; font - family : inherit ; outline : none }
# sirius - register - modal . sheet textarea { min - height : 76 px ; resize : vertical ; font - family : ui - monospace , monospace ; font - size : 13 px }
# sirius - register - modal . sheet input : focus ,
# sirius - register - modal . sheet textarea : focus { border - color : # 4 b7bec }
# sirius - register - modal . row { display : flex ; gap : 10 px ; flex - wrap : wrap ; margin - top : 18 px }
# sirius - register - modal . row . end { justify - content : flex - end }
# sirius - register - modal . x { position : absolute ; top : 14 px ; right : 18 px ; background : none ; border : none ; color : var ( -- dim , # 5e6678 ) ; font - size : 22 px ; cursor : pointer }
# sirius - register - modal . sheetwrap { position : relative ; width : 100 % ; max - width : 620 px }
# sirius - register - modal . phrase { display : grid ; grid - template - columns : repeat ( 3 , 1 fr ) ; gap : 8 px ; margin - top : 10 px }
# sirius - register - modal . phrase span { background : # 0e131 b ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - radius : 8 px ; padding : 8 px 10 px ; font - family : ui - monospace , monospace ; font - size : 13 px }
# sirius - register - modal . phrase span b { color : var ( -- dim , # 5e6678 ) ; font - weight : 400 ; margin - right : 6 px ; font - size : 11 px }
# sirius - register - modal . addr { display : flex ; gap : 8 px ; align - items : center ; margin - top : 8 px }
# sirius - register - modal . addr code { flex : 1 ; word - break : break - all ; background : # 0e131 b ;
border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; border - radius : 8 px ; padding : 9 px 11 px ;
font - family : ui - monospace , monospace ; font - size : 12.5 px }
# sirius - register - modal table . price { width : 100 % ; border - collapse : collapse ; margin - top : 12 px ; font - size : 14 px }
# sirius - register - modal table . price td { padding : 7 px 0 ; border - bottom : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; color : var ( -- mut , # 8 b98a9 ) }
# sirius - register - modal table . price td : last - child { text - align : right ; font - family : ui - monospace , monospace ; color : var ( -- ink , # e7eaf1 ) }
# sirius - register - modal table . price tr . total td { border - bottom : none ; color : var ( -- ink , # e7eaf1 ) ; font - weight : 600 ; padding - top : 12 px }
# sirius - register - modal table . price tr . total td : last - child { color : var ( -- acid , # d6ff3d ) }
# sirius - register - modal table . price td small { display : block ; color : var ( -- dim , # 5e6678 ) ; font - size : 11.5 px }
# sirius - register - modal . steps - log { margin - top : 14 px ; font - family : ui - monospace , monospace ; font - size : 12.5 px ;
color : var ( -- mut , # 8 b98a9 ) ; background : # 0e131 b ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - radius : 10 px ; padding : 12 px 14 px ; max - height : 180 px ; overflow - y : auto }
# sirius - register - modal . steps - log div : : before { content : "› " ; color : var ( -- acid , # d6ff3d ) }
# sirius - register - modal . err { color : var ( -- taken , # f6768a ) ; font - size : 13.5 px ; margin - top : 12 px }
# sirius - register - modal . ok { color : var ( -- ok , # 4 fd1a5 ) }
# sirius - register - modal . pill { display : inline - block ; font - size : 11 px ; padding : 2 px 8 px ; border - radius : 999 px ;
background : rgba ( 214 , 255 , 61 , . 13 ) ; color : var ( -- acid , # d6ff3d ) ; margin - left : 8 px ; vertical - align : middle }
# sirius - register - modal . pill . soon { background : rgba ( 255 , 255 , 255 , . 07 ) ; color : var ( -- dim , # 5e6678 ) }
# sirius - register - modal . wopt { display : flex ; gap : 14 px ; align - items : flex - start ; background : var ( -- panel2 , # 18202 c ) ;
border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; border - radius : 12 px ; padding : 14 px 16 px ; cursor : pointer ;
text - align : left ; width : 100 % ; color : inherit ; font : inherit ; margin - top : 10 px }
# sirius - register - modal . wopt : hover { border - color : # 4 b7bec }
# sirius - register - modal . wopt : disabled { opacity : . 45 ; cursor : not - allowed }
# sirius - register - modal . wopt . ic { font - size : 22 px ; line - height : 1 }
# sirius - register - modal . wopt b { display : block ; font - size : 14.5 px }
# sirius - register - modal . wopt span { color : var ( -- mut , # 8 b98a9 ) ; font - size : 13 px }
# sirius - register - modal . chk { display : flex ; gap : 9 px ; align - items : flex - start ; margin - top : 14 px ; font - size : 13.5 px ; color : var ( -- mut , # 8 b98a9 ) }
# sirius - register - modal . chk input { margin - top : 3 px }
2026-09-08 02:19:41 +02:00
# sirius - register - modal . signin - tabs {
display : flex ; gap : 2 px ; margin : - 8 px - 12 px 18 px ; padding : 0 4 px 0 ;
border - bottom : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; overflow - x : auto
}
# sirius - register - modal . signin - tabs button {
background : none ; border : none ; color : var ( -- mut , # 8 b98a9 ) ; font - family : inherit ; font - size : 12.5 px ;
padding : 10 px 12 px ; cursor : pointer ; border - bottom : 2 px solid transparent ;
white - space : nowrap ; letter - spacing : . 2 px ; transition : color . 1 s , border - color . 1 s
}
# sirius - register - modal . signin - tabs button : hover { color : var ( -- ink , # e7eaf1 ) }
# sirius - register - modal . signin - tabs button . active {
color : var ( -- acid , # d6ff3d ) ; border - bottom - color : var ( -- acid , # d6ff3d )
}
2026-09-08 13:06:16 +02:00
/ * I n l i n e - s h e e t v a r i a n t : s a m e s t e p c o n t e n t s t y l i n g a s t h e m o d a l , b u t
rendered as a normal page section ( no fixed positioning , no backdrop ,
no shadow ) . Portal . html uses this so the tabbed sign - in IS the page . * /
. sirius - inline - sheet {
background : var ( -- panel , # 141 a24 ) ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - radius : 16 px ; padding : 22 px 24 px ; color : var ( -- ink , # e7eaf1 )
}
. sirius - inline - sheet h3 { margin : 0 0 . 2 rem ; font - size : 1.15 rem }
. sirius - inline - sheet . sub { color : var ( -- mut , # 8 b98a9 ) ; font - size : 14 px ; margin : 0 0 1.2 rem }
. sirius - inline - sheet label { display : block ; font - size : 13 px ; color : var ( -- mut , # 8 b98a9 ) ; margin : 14 px 0 5 px }
. sirius - inline - sheet input [ type = text ] ,
. sirius - inline - sheet input [ type = password ] ,
. sirius - inline - sheet textarea ,
. sirius - inline - sheet select {
width : 100 % ; padding : 11 px 13 px ; border - radius : 10 px ; border : 1 px solid # ffffff22 ; background : # 0e131 b ;
color : var ( -- ink , # e7eaf1 ) ; font - size : 14 px ; font - family : inherit ; outline : none
}
. sirius - inline - sheet textarea { min - height : 76 px ; resize : vertical ; font - family : ui - monospace , monospace ; font - size : 13 px }
. sirius - inline - sheet input : focus , . sirius - inline - sheet textarea : focus { border - color : # 4 b7bec }
. sirius - inline - sheet . row { display : flex ; gap : 10 px ; flex - wrap : wrap ; margin - top : 18 px }
. sirius - inline - sheet . row . end { justify - content : flex - end }
. sirius - inline - sheet . phrase { display : grid ; grid - template - columns : repeat ( 3 , 1 fr ) ; gap : 8 px ; margin - top : 10 px }
. sirius - inline - sheet . phrase span { background : # 0e131 b ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - radius : 8 px ; padding : 8 px 10 px ; font - family : ui - monospace , monospace ; font - size : 13 px }
. sirius - inline - sheet . phrase span b { color : var ( -- dim , # 5e6678 ) ; font - weight : 400 ; margin - right : 6 px ; font - size : 11 px }
. sirius - inline - sheet . addr { display : flex ; gap : 8 px ; align - items : center ; margin - top : 8 px }
. sirius - inline - sheet . addr code { flex : 1 ; word - break : break - all ; background : # 0e131 b ;
border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; border - radius : 8 px ; padding : 9 px 11 px ;
font - family : ui - monospace , monospace ; font - size : 12.5 px }
. sirius - inline - sheet . steps - log { margin - top : 14 px ; font - family : ui - monospace , monospace ; font - size : 12.5 px ;
color : var ( -- mut , # 8 b98a9 ) ; background : # 0e131 b ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - radius : 10 px ; padding : 12 px 14 px ; max - height : 180 px ; overflow - y : auto }
. sirius - inline - sheet . steps - log div : : before { content : "› " ; color : var ( -- acid , # d6ff3d ) }
. sirius - inline - sheet . err { color : var ( -- taken , # f6768a ) ; font - size : 13.5 px ; margin - top : 12 px }
. sirius - inline - sheet . ok { color : var ( -- ok , # 4 fd1a5 ) }
. sirius - inline - sheet . note { background : var ( -- panel2 , # 18202 c ) ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - left : 3 px solid var ( -- acid , # d6ff3d ) ; border - radius : 10 px ; padding : 14 px 18 px ;
color : var ( -- mut , # 8 b98a9 ) ; font - size : 14 px ; margin - top : 1.4 rem }
. sirius - inline - sheet . note . warn { border - left - color : var ( -- warn , # ffc75f ) }
. sirius - inline - sheet . note b { color : var ( -- ink , # e7eaf1 ) }
. sirius - inline - sheet . mono { background : # 0e131 b ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - radius : 6 px ; padding : 1 px 6 px ; font - family : ui - monospace , monospace ; font - size : 13 px }
. sirius - inline - sheet . chk { display : flex ; gap : 9 px ; align - items : flex - start ; margin - top : 14 px ; font - size : 13.5 px ; color : var ( -- mut , # 8 b98a9 ) }
. sirius - inline - sheet . chk input { margin - top : 3 px }
. sirius - inline - sheet . signin - tabs {
display : flex ; gap : 2 px ; margin : - 6 px - 8 px 18 px ; padding : 0 4 px ;
border - bottom : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; overflow - x : auto
}
. sirius - inline - sheet . signin - tabs button {
background : none ; border : none ; color : var ( -- mut , # 8 b98a9 ) ; font - family : inherit ; font - size : 13 px ;
padding : 10 px 14 px ; cursor : pointer ; border - bottom : 2 px solid transparent ;
white - space : nowrap ; letter - spacing : . 2 px ; transition : color . 1 s , border - color . 1 s
}
. sirius - inline - sheet . signin - tabs button : hover { color : var ( -- ink , # e7eaf1 ) }
. sirius - inline - sheet . signin - tabs button . active {
color : var ( -- acid , # d6ff3d ) ; border - bottom - color : var ( -- acid , # d6ff3d )
}
. sirius - inline - sheet . btn { display : inline - block ; text - decoration : none ; padding : 10 px 18 px ; border - radius : 10 px ;
background : # 4 b7bec ; color : # fff ; font - size : 14 px ; border : none ; cursor : pointer ; font - family : inherit }
. sirius - inline - sheet . btn . ghost { background : transparent ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; color : var ( -- ink , # e7eaf1 ) }
. sirius - inline - sheet . btn . acid { background : var ( -- acid , # d6ff3d ) ; color : # 0b0 e14 ; font - weight : 600 }
. sirius - inline - sheet . btn : hover { filter : brightness ( 1.12 ) }
. sirius - inline - sheet . btn : disabled { opacity : . 45 ; cursor : not - allowed ; filter : none }
. sirius - inline - sheet . wopt { display : flex ; gap : 14 px ; align - items : flex - start ; background : var ( -- panel2 , # 18202 c ) ;
border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; border - radius : 12 px ; padding : 14 px 16 px ; cursor : pointer ;
text - align : left ; width : 100 % ; color : inherit ; font : inherit ; margin - top : 10 px }
. sirius - inline - sheet . wopt : hover { border - color : # 4 b7bec }
. sirius - inline - sheet . wopt . ic { font - size : 22 px ; line - height : 1 }
. sirius - inline - sheet . wopt b { display : block ; font - size : 14.5 px }
. sirius - inline - sheet . wopt span { color : var ( -- mut , # 8 b98a9 ) ; font - size : 13 px }
2026-09-07 22:36:02 +02:00
# sirius - register - modal . btn { display : inline - block ; text - decoration : none ; padding : 10 px 18 px ; border - radius : 10 px ;
background : # 4 b7bec ; color : # fff ; font - size : 14 px ; border : none ; cursor : pointer ; font - family : inherit }
# sirius - register - modal . btn . ghost { background : transparent ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ; color : var ( -- ink , # e7eaf1 ) }
# sirius - register - modal . btn . acid { background : var ( -- acid , # d6ff3d ) ; color : # 0b0 e14 ; font - weight : 600 }
# sirius - register - modal . btn : hover { filter : brightness ( 1.12 ) }
# sirius - register - modal . btn : disabled { opacity : . 45 ; cursor : not - allowed ; filter : none }
# sirius - register - modal . mono { background : # 0e131 b ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - radius : 6 px ; padding : 1 px 6 px ; font - family : ui - monospace , monospace ; font - size : 13 px }
# sirius - register - modal . muted { color : var ( -- dim , # 5e6678 ) ; font - size : 13 px }
# sirius - register - modal . note { background : var ( -- panel , # 141 a24 ) ; border : 1 px solid var ( -- line , rgba ( 255 , 255 , 255 , . 09 ) ) ;
border - left : 3 px solid var ( -- acid , # d6ff3d ) ; border - radius : 10 px ; padding : 14 px 18 px ;
color : var ( -- mut , # 8 b98a9 ) ; font - size : 14 px ; margin - top : 1.4 rem }
# sirius - register - modal . note b { color : var ( -- ink , # e7eaf1 ) }
# sirius - register - modal . note . warn { border - left - color : var ( -- warn , # ffc75f ) }
` ;
document . head . appendChild ( css ) ;
const modal = document . createElement ( "div" ) ;
modal . id = "sirius-register-modal" ;
modal . innerHTML = `
< div class = "sheetwrap" >
< button class = "x" id = "sirius-reg-close" title = "close" > × < / b u t t o n >
< div class = "sheet" id = "sirius-reg-sheet" > < / d i v >
< / d i v >
` ;
document . body . appendChild ( modal ) ;
} ) ( ) ;
// ---------- flow state ----------
2026-09-07 23:52:12 +02:00
// signInOnly: when true, the flow is used for wallet onboarding from the nav
// dropdown — no name to mint, so instead of walking through Fund/Confirm/
// Register we stop at 'wallet is ready', write siriusProfile, and close.
2026-09-08 13:06:16 +02:00
// inlineTarget: when set, the flow renders into that page element instead
// of the modal sheet (portal.html uses this to embed the tabbed sign-in as
// the page's main content). The modal is left closed in inline mode.
const state = { name : null , signInOnly : false , inlineTarget : null , wallet : null , session : null , client : null , quote : null , result : null } ;
2026-09-07 22:36:02 +02:00
const modal = $ ( "sirius-register-modal" ) ;
2026-09-08 13:06:16 +02:00
const modalSheet = $ ( "sirius-reg-sheet" ) ;
// `sheet` is the element the step functions render into — the modal's sheet
// by default, or an inline container when siriusRenderSignInInline() is
// active. `let` so it can be reassigned per call.
let sheet = modalSheet ;
2026-09-07 22:36:02 +02:00
2026-09-08 13:06:16 +02:00
function open ( ) {
// Inline mode never opens the modal — the sign-in UI lives on the page.
if ( state . inlineTarget ) return ;
modal . classList . add ( "open" ) ;
}
2026-09-07 22:36:02 +02:00
function close ( ) {
2026-09-08 13:06:16 +02:00
// In inline mode we can't "close" — leave the content in place. State is
// still reset so the next call starts fresh.
if ( ! state . inlineTarget ) modal . classList . remove ( "open" ) ;
2026-09-07 22:36:02 +02:00
if ( state . client ) { try { state . client . close ( ) ; } catch { } state . client = null ; }
2026-09-07 23:52:12 +02:00
// Keep the WC session alive when signing in — later record edits or TLD
// mints will need it; only tear it down for the mint flow.
if ( state . session && ! state . signInOnly ) { try { state . session . disconnect ( ) ; } catch { } state . session = null ; }
state . signInOnly = false ;
2026-09-08 13:06:16 +02:00
// Restore the modal sheet as the default render target so subsequent
// register/mint flows (which are modal-based) work.
if ( state . inlineTarget ) {
state . inlineTarget = null ;
sheet = modalSheet ;
}
2026-09-07 23:52:12 +02:00
}
// Persist wallet identity so profile-menu.js and any other tab see the
// signed-in state immediately (same as portal.html's writeProfile).
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" ) ) ;
}
// A wallet was just loaded/created/connected. If we are in sign-in-only
// mode, mark siriusProfile and close the modal here; otherwise return
// false so the caller keeps going to Fund → Confirm → Register.
function finishSignIn ( w ) {
feat(sirius-x/signin): PIN escrow — encrypted-at-rest quick unlock, 3-strike password fallback
Removes the sessionStorage plaintext-mnemonic cache (fixed under the
same commit) and replaces it with a PIN-encrypted blob in localStorage.
No plaintext secret ever touches disk or memory outside the live
BuiltInWallet object.
New js/pin-escrow.js — WebCrypto PBKDF2(50k) + AES-GCM(256). Public
API on window.siriusPin: savePinBlob(mnemonic, pin), tryUnlock(pin),
hasPin(), attemptsUsed(), attemptsRemaining(), clear(), MAX_ATTEMPTS.
Iteration count is lighter than BuiltInWallet's 250k because a 4-6
digit PIN's key space is small anyway; the point is 'not plaintext at
rest,' not brute-force resistance — the durable secret is the full
password.
register-flow.js:
- After a fresh password unlock (Import / Create / Unlock), stepSetPin
offers a 4-6 digit PIN with confirm — skippable with 'Not now'.
Never overwrites an existing PIN blob.
- stepUnlock now shows a numeric PIN pad when a PIN blob is present;
the password field only appears when the user opts to 'Use password
instead' or after the blob was wiped.
- Wrong PIN → increment counter, surface 'N attempts left'. Third
wrong PIN → wipe blob and route to a 'PIN reset' screen that hands
off to the password form.
- Correct PIN → decrypt the mnemonic in-browser, rebuild the
BuiltInWallet, finishSignIn(). Attempt counter resets to 0.
profile-menu.js:
- Sign-out clears the PIN blob (via siriusPin.clear()) alongside the
siriusProfile so the device isn't quick-unlockable with a stale PIN.
All pages that host the sign-in flow now include pin-escrow.js. Same
tabbed layout in stepUnlock — the PIN pad and the password field both
live under 🔓 Unlock, transparent tab-switch works exactly as before.
Verified live:
- Fresh Import → 'Set a PIN' step → 4242 confirmed → blob written
- Reload → PIN pad, 3 attempts remaining
- Correct PIN 4242 → signed in, counter resets to 0
- 3 wrong PIN attempts → 'Wrong PIN — N attempts left' per attempt,
then 'PIN locked — enter your full password to continue', blob
wiped, next reload shows the password form
- localStorage contains only ciphertext + salt + iv + counter; no
plaintext mnemonic anywhere on disk or in sessionStorage.
2026-09-09 01:31:18 +02:00
// Expose the live wallet in memory only, BEFORE writeProfile so any
// listener of siriusProfileChanged (portal.html adoptWalletFromModal)
// sees it and can switch to the signed-in view synchronously. The
// mnemonic is NEVER written to sessionStorage / localStorage in
// plaintext — the only at-rest copies are the password-encrypted
// BuiltInWallet blob and, optionally, the PIN-encrypted escrow blob.
2026-09-08 02:35:56 +02:00
window . siriusWallet = w ;
2026-09-09 01:15:14 +02:00
writeProfile ( { address : w . address , tokenAddress : w . tokenAddress , source : w . source ? ? "seed" } ) ;
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
// Persistent device session (js/session.js): remember the phrase under a
// non-extractable browser key so the next visit signs in silently. Only
// for the built-in wallet — external (WizardConnect) sessions cannot be
// resumed without the other wallet.
if ( w ? . mnemonic && window . siriusSession ? . remember ) {
window . siriusSession . remember ( w . mnemonic , { accountPath : w . accountPath } ) . catch ( ( ) => { } ) ;
}
2026-09-07 23:52:12 +02:00
if ( state . signInOnly ) {
render ( ` <h3><span class="ok">✓</span> Signed in</h3>
< p class = "sub" > You are signed in as < span class = "mono" > $ { esc ( w . address ) } < / s p a n > . N a m e s y o u o w n
will appear in the wallet dropdown ; the same wallet signs record edits and new registrations . < / p >
< div class = "row end" > < button class = "btn acid" id = "fin" > Done < / b u t t o n > < / d i v > ` ) ;
$ ( "fin" ) . onclick = close ;
return true ;
}
return false ;
2026-09-07 22:36:02 +02:00
}
$ ( "sirius-reg-close" ) . onclick = close ;
modal . addEventListener ( "click" , ( e ) => { if ( e . target === modal ) close ( ) ; } ) ;
2026-09-08 13:06:16 +02:00
// Step functions render into `sheet`, which is reassigned per call by
// renderSignInInline() when the flow is mounted inline on a host page
// (portal.html). Modal by default. Direct write — no separate variable
// needed since `sheet` above is the single mutable target.
2026-09-07 22:36:02 +02:00
function render ( html ) { sheet . innerHTML = html ; }
2026-09-08 02:19:41 +02:00
// Tab bar for sign-in mode — lets the user jump between the four wallet
// actions without going back to a wallet-choice screen. Prepended to the
// sheet content by each sign-in step. Only shown in signInOnly mode.
function renderTabs ( active ) {
if ( ! state . signInOnly ) return "" ;
const saved = BNS . BuiltInWallet . exists ( ) ;
2026-09-09 00:47:32 +02:00
// The four tabs are always present so the layout is stable across sessions.
// When there is no saved wallet on this device, the Unlock tab still shows
// — clicking it lands on a stepUnlock screen that explains why unlock is
// not available and offers a shortcut to Create or Import instead.
const tabs = [
[ "unlock" , "🔓 Unlock" , saved ? "Wallet stored on this device" : "No saved wallet on this device" ] ,
[ "new" , "🆕 Create" , "Sign up · fresh phrase" ] ,
[ "import" , "📥 Import" , "Sign in · seed you have" ] ,
[ "wc" , "🔗 WizardConnect" , "Cashonize / Paytaca" ] ,
] ;
2026-09-08 02:19:41 +02:00
return `
< div class = "signin-tabs" role = "tablist" >
$ { tabs . map ( ( [ id , label , hint ] ) => `
< button role = "tab" data - tab = "${id}" class = "${id === active ? " active " : " "}"
title = "${esc(hint)}" > $ { label } < / b u t t o n >
` ).join("")}
< / d i v >
` ;
}
2026-09-08 13:06:16 +02:00
// Wire tab clicks — document-level delegation so both the modal sheet and
// any inline container hosting the tabs work with one listener. Switching
// away from a live WC session tears it down first so we don't leak
// connections. Guarded to only match .signin-tabs buttons we render.
document . addEventListener ( "click" , async ( e ) => {
const t = e . target . closest ( ".signin-tabs [data-tab]" ) ;
2026-09-08 02:19:41 +02:00
if ( ! t ) return ;
const which = t . dataset . tab ;
if ( which !== "wc" && state . session ) {
try { await state . session . disconnect ( ) ; } catch { }
state . session = null ;
}
if ( which === "unlock" ) stepUnlock ( ) ;
else if ( which === "new" ) stepCreate ( ) ;
else if ( which === "import" ) stepImport ( ) ;
else if ( which === "wc" ) stepExternal ( ) ;
} ) ;
2026-09-07 22:36:02 +02:00
function err ( e ) {
sheet . querySelectorAll ( ".err" ) . forEach ( ( n ) => n . remove ( ) ) ;
const box = document . createElement ( "div" ) ;
box . className = "err" ;
box . textContent = e ? . message ? ? String ( e ) ;
sheet . appendChild ( box ) ;
}
async function client ( ) {
if ( ! state . client ) state . client = await BNS . connect ( ) ;
return state . client ;
}
feat(sirius-x): show mint prices; reuse unlocked wallet for TLD/name mint
Two things:
1. Reuse the wallet the user already unlocked (adoptSignedInWallet).
startFlow / startTldFlow were always calling stepWallet(), which
showed the four-option wallet-choice and then prompted for the
password again on 'Unlock'. Right after signing in, that meant
entering the password twice to buy a TLD. Now both flows check
window.siriusWallet at start and, if it's populated (built-in
wallet in memory, or a WC-wrapper with a live session), skip
straight to stepFund with state.wallet already set. Password is
only asked when there is no in-memory wallet to reuse.
2. Prices are visible on the landing card + tld.html search:
- name: '≈ 10,000 sat service fee · 1,300 sat chain dust · < 1 ¢'
with a note that the 90/10 split applies (TLD owner gets 90 %)
- TLD: '≈ 1,250,000 sat · ≈ ' with the incentive line
('you earn 90 % of the fee on every name registered under
your TLD — forever, no renewals')
Both labelled 'chipnet placeholder' — mainnet will price by
label length via an on-chain oracle.
PIN as a lighter approval than the full password is a bigger design
change (encryption key derivation, cache lifetime, revocation) — not
in this commit; adopting the already-unlocked in-memory wallet is
the fix that removes the password-again symptom for now.
2026-09-09 00:30:05 +02:00
// If the user already unlocked a wallet in this tab (via inline sign-in on
// portal.html or a prior mint), the register/TLD mint flow can skip the
// wallet-choice step entirely and go straight to Fund / Confirm. That's
// what stops the flow from asking for the password a second time on a
// TLD buy right after unlocking.
function adoptSignedInWallet ( ) {
if ( window . siriusWallet && ! state . wallet ) {
state . wallet = window . siriusWallet ;
// WC-signed-in wallets carry a `session`; keep it plumbed for the mint.
if ( window . siriusWallet . source === "wc" && window . siriusWallet . session ) {
state . session = window . siriusWallet . session ;
}
return true ;
}
return false ;
}
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
// Return the live wallet if one is already in memory; otherwise try the
// persistent device session (js/session.js) — the phrase is decrypted with
// a browser-held key, never stored in plaintext. Falls back to false, which
// sends the flow to the wallet-choice / Unlock screen.
let restoring = null ;
async function restoreSessionWallet ( ) {
if ( window . siriusWallet ) return window . siriusWallet ;
if ( ! window . siriusSession ? . restore ) return null ;
if ( ! restoring ) {
restoring = ( async ( ) => {
try {
const s = await window . siriusSession . restore ( ) ;
if ( ! s ? . mnemonic ) return null ;
const w = s . accountPath
? await BNS . BuiltInWallet . fromMnemonic ( s . mnemonic , undefined , s . accountPath )
: await BNS . BuiltInWallet . fromMnemonic ( s . mnemonic ) ;
if ( ! window . siriusWallet ) {
window . siriusWallet = w ;
// Keep the profile breadcrumb in step (address may have been
// cleared by a sign-out in another tab that did not reach here).
writeProfile ( { address : w . address , tokenAddress : w . tokenAddress , source : "seed" } ) ;
}
return window . siriusWallet ;
} catch { return null ; }
finally { restoring = null ; }
} ) ( ) ;
}
return restoring ;
}
window . siriusRestoreSession = restoreSessionWallet ;
2026-09-09 01:15:14 +02:00
async function adoptSessionWalletAsync ( ) {
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 ( adoptSignedInWallet ( ) ) return true ;
const w = await restoreSessionWallet ( ) ;
if ( w ) return adoptSignedInWallet ( ) ;
return false ;
2026-09-09 01:15:14 +02:00
}
async function startFlow ( name , opts = { } ) {
2026-09-07 22:36:02 +02:00
state . name = name ;
2026-09-08 00:10:11 +02:00
state . tld = null ;
state . signInOnly = false ;
feat(sirius-x): TLD owner panel — owner-set price, on/off switch, private TLDs
A TLD owner needed to run their namespace without the operator: set what
every name under the TLD sells for, take the TLD off the public registry
for a while, and still register names under it themselves.
Both settings live on chain in the TLD's TUPD records (`price`, `hidden`)
because changes are rare and every client already walks the TLD beacon.
The gateway's /api/tlds now carries records, owner, price_usd and
hidden_by per TLD; pricing.js quotes the owner price ahead of the length
tiers and only trusts a TLD_BEACON-sourced list; the register flow refuses
hidden and frozen TLDs for the public but lets the owner through at the
platform share only (the 90% owner cut would be paid to themselves). The
portal's TLDs tab loads real holdings, shows price and on/off state, and
gives each TLD a one-click switch, a price/policy editor and an inline
"register a name under .tld" form. Docs and the design table describe the
two new records.
2026-09-16 21:28:18 +02:00
// Service fee: the TLD owner's flat price when they set one, else the
// length tier (siriusPricing owns both so landing search badges, tld
// cards and this mint agree). Caller can override with
2026-09-09 01:09:05 +02:00
// opts.serviceFeeSats when a coupon or manual price is in play.
feat(sirius-x): TLD owner panel — owner-set price, on/off switch, private TLDs
A TLD owner needed to run their namespace without the operator: set what
every name under the TLD sells for, take the TLD off the public registry
for a while, and still register names under it themselves.
Both settings live on chain in the TLD's TUPD records (`price`, `hidden`)
because changes are rare and every client already walks the TLD beacon.
The gateway's /api/tlds now carries records, owner, price_usd and
hidden_by per TLD; pricing.js quotes the owner price ahead of the length
tiers and only trusts a TLD_BEACON-sourced list; the register flow refuses
hidden and frozen TLDs for the public but lets the owner through at the
platform share only (the 90% owner cut would be paid to themselves). The
portal's TLDs tab loads real holdings, shows price and on/off state, and
gives each TLD a one-click switch, a price/policy editor and an inline
"register a name under .tld" form. Docs and the design table describe the
two new records.
2026-09-16 21:28:18 +02:00
const [ label , tldRaw ] = String ( name ) . split ( "." ) ;
const tld = tldRaw || "bch" ;
const sp = window . siriusPricing ;
if ( sp ? . tldReady ) await sp . tldReady ( ) ;
// A TLD its owner switched off is private: the public cannot buy under
// it, but the owner still can (from the portal's TLDs tab). Frozen means
// nobody, owner included. Check against the signed-in wallet's addresses
// so the owner's own purchase goes through.
const mine = ( window . siriusWallet ? . watchedAddresses )
|| ( window . siriusWallet ? . address ? [ window . siriusWallet . address ] : [ ] )
|| [ ] ;
const sell = sp ? . tldSellable ? sp . tldSellable ( tld , mine ) : { ok : true , owner : false } ;
if ( ! sell . ok ) {
open ( ) ;
const why = sell . reason === "frozen"
? ` The owner of <b>. ${ esc ( tld ) } </b> has frozen it — no new names are being registered under it right now. `
: ` <b>. ${ esc ( tld ) } </b> is a private TLD — its owner has switched it off for the public. Only the owner can register names under it right now. Check back later or pick another TLD. ` ;
render ( ` <h3> ${ esc ( name ) } is not available</h3><p class="sub"> ${ why } </p>
< div class = "row end" > < button class = "btn ghost" id = "back" > Close < / b u t t o n > < / d i v > ` ) ;
$ ( "back" ) . onclick = close ;
return ;
}
const p = sp ? . priceForName ( label , tld ) ;
state . ownerMint = ! ! sell . owner ;
// Owner registering under their own TLD: they would receive 90% of the
// fee from themselves, so only the platform's share is charged. The
// registrar lib already routes the whole fee to the platform when the
// TLD owner is the buyer (no self-payment output).
const ownerRate = ( sats ) => ( sats == null ? null : BigInt ( sats ) / 10 n ) ;
2026-09-09 01:09:05 +02:00
state . nameFeeSats = opts . serviceFeeSats != null
? BigInt ( opts . serviceFeeSats )
feat(sirius-x): TLD owner panel — owner-set price, on/off switch, private TLDs
A TLD owner needed to run their namespace without the operator: set what
every name under the TLD sells for, take the TLD off the public registry
for a while, and still register names under it themselves.
Both settings live on chain in the TLD's TUPD records (`price`, `hidden`)
because changes are rare and every client already walks the TLD beacon.
The gateway's /api/tlds now carries records, owner, price_usd and
hidden_by per TLD; pricing.js quotes the owner price ahead of the length
tiers and only trusts a TLD_BEACON-sourced list; the register flow refuses
hidden and frozen TLDs for the public but lets the owner through at the
platform share only (the 90% owner cut would be paid to themselves). The
portal's TLDs tab loads real holdings, shows price and on/off state, and
gives each TLD a one-click switch, a price/policy editor and an inline
"register a name under .tld" form. Docs and the design table describe the
two new records.
2026-09-16 21:28:18 +02:00
: ( p ? ( sell . owner ? ownerRate ( p . sats ) : p . sats ) : null ) ;
2026-09-08 00:10:11 +02:00
open ( ) ;
2026-09-09 01:15:14 +02:00
if ( await adoptSessionWalletAsync ( ) ) stepFund ( ) ;
feat(sirius-x): show mint prices; reuse unlocked wallet for TLD/name mint
Two things:
1. Reuse the wallet the user already unlocked (adoptSignedInWallet).
startFlow / startTldFlow were always calling stepWallet(), which
showed the four-option wallet-choice and then prompted for the
password again on 'Unlock'. Right after signing in, that meant
entering the password twice to buy a TLD. Now both flows check
window.siriusWallet at start and, if it's populated (built-in
wallet in memory, or a WC-wrapper with a live session), skip
straight to stepFund with state.wallet already set. Password is
only asked when there is no in-memory wallet to reuse.
2. Prices are visible on the landing card + tld.html search:
- name: '≈ 10,000 sat service fee · 1,300 sat chain dust · < 1 ¢'
with a note that the 90/10 split applies (TLD owner gets 90 %)
- TLD: '≈ 1,250,000 sat · ≈ ' with the incentive line
('you earn 90 % of the fee on every name registered under
your TLD — forever, no renewals')
Both labelled 'chipnet placeholder' — mainnet will price by
label length via an on-chain oracle.
PIN as a lighter approval than the full password is a bigger design
change (encryption key derivation, cache lifetime, revocation) — not
in this commit; adopting the already-unlocked in-memory wallet is
the fix that removes the password-again symptom for now.
2026-09-09 00:30:05 +02:00
else stepWallet ( ) ;
2026-09-08 00:10:11 +02:00
}
// Start a TLD mint. Same wallet-choice + fund flow as a name register, but
// the mint step calls registerTldWith(Built|External)Wallet instead of the
// name variant, and the "done" screen skips the record editor since a TLD
// certificate goes to the operator's token address as-is.
2026-09-09 01:15:14 +02:00
async function startTldFlow ( label , opts = { } ) {
2026-09-08 00:10:11 +02:00
state . name = ` . ${ label } ` ; // used for headings only
state . tld = { label , serviceFeeSats : BigInt ( opts . serviceFeeSats ? ? 0 n ) } ;
2026-09-07 23:52:12 +02:00
state . signInOnly = false ;
2026-09-07 22:36:02 +02:00
open ( ) ;
2026-09-09 01:15:14 +02:00
if ( await adoptSessionWalletAsync ( ) ) stepFund ( ) ;
feat(sirius-x): show mint prices; reuse unlocked wallet for TLD/name mint
Two things:
1. Reuse the wallet the user already unlocked (adoptSignedInWallet).
startFlow / startTldFlow were always calling stepWallet(), which
showed the four-option wallet-choice and then prompted for the
password again on 'Unlock'. Right after signing in, that meant
entering the password twice to buy a TLD. Now both flows check
window.siriusWallet at start and, if it's populated (built-in
wallet in memory, or a WC-wrapper with a live session), skip
straight to stepFund with state.wallet already set. Password is
only asked when there is no in-memory wallet to reuse.
2. Prices are visible on the landing card + tld.html search:
- name: '≈ 10,000 sat service fee · 1,300 sat chain dust · < 1 ¢'
with a note that the 90/10 split applies (TLD owner gets 90 %)
- TLD: '≈ 1,250,000 sat · ≈ ' with the incentive line
('you earn 90 % of the fee on every name registered under
your TLD — forever, no renewals')
Both labelled 'chipnet placeholder' — mainnet will price by
label length via an on-chain oracle.
PIN as a lighter approval than the full password is a bigger design
change (encryption key derivation, cache lifetime, revocation) — not
in this commit; adopting the already-unlocked in-memory wallet is
the fix that removes the password-again symptom for now.
2026-09-09 00:30:05 +02:00
else stepWallet ( ) ;
2026-09-07 22:36:02 +02:00
}
2026-09-07 23:52:12 +02:00
// Sign-in-only entry point for the nav wallet dropdown. mode = 'new' opens
// the "create wallet" step directly, 'import' opens "import phrase", 'wc'
// starts a WizardConnect session. On success the flow writes siriusProfile
// and closes — no name context, no Fund/Confirm.
function startSignIn ( mode ) {
2026-09-08 01:43:35 +02:00
state . name = "your wallet" ; // header text — used by stepWallet
2026-09-08 00:24:54 +02:00
state . tld = null ;
2026-09-07 23:52:12 +02:00
state . signInOnly = true ;
open ( ) ;
2026-09-08 01:43:35 +02:00
// "choose" (default) shows the wallet-choice screen with all four options;
// the other modes are still supported as direct-action entry points from
// deep-links like portal.html?mode=new — but the nav dropdown funnels to
// "choose" so the user always sees the options first.
2026-09-07 23:52:12 +02:00
if ( mode === "new" ) stepCreate ( ) ;
else if ( mode === "import" ) stepImport ( ) ;
else if ( mode === "wc" ) stepExternal ( ) ;
2026-09-08 00:24:54 +02:00
else if ( mode === "unlock" ) stepUnlock ( ) ;
2026-09-07 23:52:12 +02:00
else stepWallet ( ) ;
}
2026-09-08 13:06:16 +02:00
// Sign-in rendered inline on the host page (not in a modal). Pass a target
// element and portal-style tabs + form get painted into it. Used by
// portal.html so the "sign up / sign in" experience IS the page rather than
// an overlay on top of a launcher card.
function renderSignInInline ( container , mode = "import" ) {
if ( ! container ) return ;
state . name = "your wallet" ;
state . tld = null ;
state . signInOnly = true ;
state . inlineTarget = container ;
sheet = container ;
// Give inline container the same visual bones as the modal sheet so the
// step content styling ports 1:1. Kept as a class so host CSS can override.
container . classList . add ( "sirius-inline-sheet" ) ;
if ( mode === "new" ) stepCreate ( ) ;
else if ( mode === "import" ) stepImport ( ) ;
else if ( mode === "wc" ) stepExternal ( ) ;
else if ( mode === "unlock" ) stepUnlock ( ) ;
else stepWallet ( ) ;
}
2026-09-07 22:36:02 +02:00
// Public API
window . siriusRegisterName = startFlow ;
2026-09-08 00:10:11 +02:00
window . siriusRegisterTld = startTldFlow ;
2026-09-07 23:52:12 +02:00
window . siriusSignInWallet = startSignIn ;
2026-09-08 13:06:16 +02:00
window . siriusRenderSignInInline = renderSignInInline ;
2026-09-07 22:36:02 +02:00
// ---------- step 1: wallet choice ----------
function stepWallet ( ) {
const saved = BNS . BuiltInWallet . exists ( ) ;
2026-09-08 00:10:11 +02:00
const kind = state . tld ? "TLD" : "name" ;
2026-09-08 01:43:35 +02:00
const heading = state . signInOnly
? ` Sign in <span style="color:var(--acid,#d6ff3d)">or</span> create a wallet `
: ` Register ${ esc ( state . name ) } ` ;
const intro = state . signInOnly
? ` Pick one. Your keys stay in this browser — nothing is transmitted for sign-in. `
: ` First, the wallet that will <b>own</b> the ${ esc ( kind ) } . The certificate is minted straight to it — we never hold it. ` ;
2026-09-07 22:36:02 +02:00
render ( `
2026-09-08 01:43:35 +02:00
< h3 > $ { heading } < / h 3 >
< p class = "sub" > $ { intro } < / p >
2026-09-07 22:36:02 +02:00
$ { saved ? ` <button class="wopt" id="w-unlock"><span class="ic">🔓</span><span>
< b > Unlock my browser wallet < / b >
< span > You already created one on this device . < / s p a n > < / s p a n > < / b u t t o n > ` : ` ` }
< button class = "wopt" id = "w-new" > < span class = "ic" > ✨ < / s p a n > < s p a n >
< b > Create a new wallet < span class = "pill" > easiest < / s p a n > < / b >
< span > Made here in your browser . You get a recovery phrase to write down — it is the only key . < / s p a n > < / s p a n > < / b u t t o n >
< button class = "wopt" id = "w-import" > < span class = "ic" > 🔑 < / s p a n > < s p a n >
< b > Import a recovery phrase < / b >
< span > Restore a wallet you already have , from its 12 - word phrase . < / s p a n > < / s p a n > < / b u t t o n >
< button class = "wopt" id = "w-wiz" > < span class = "ic" > 🪄 < / s p a n > < s p a n >
< b > Connect a wallet — WizardConnect < span class = "pill" > most private < / s p a n > < / b >
< span > Cashonize 0.9 + or Paytaca . Encrypted end - to - end over Nostr ; the relay sees only
ciphertext . Your keys never leave your wallet . < / s p a n > < / s p a n > < / b u t t o n >
< div class = "note" style = "margin-top:18px" > Everything below runs on < b > chipnet < / b > , B i t c o i n C a s h ' s
test network . The coins are free and worth nothing — this is an alpha , and it is the honest
place to try it before real money is involved . < / d i v >
` );
if ( saved ) $ ( "w-unlock" ) . onclick = stepUnlock ;
$ ( "w-new" ) . onclick = stepCreate ;
$ ( "w-import" ) . onclick = stepImport ;
$ ( "w-wiz" ) . onclick = ( ) => stepExternal ( "wizard" ) ;
}
// ---------- WizardConnect (external wallet) ----------
async function stepExternal ( ) {
const label = "WizardConnect" ;
2026-09-08 02:19:41 +02:00
render ( ` ${ renderTabs ( "wc" ) } <h3>Connect with ${ esc ( label ) } </h3><p class="sub">Starting the connection…</p> ` ) ;
2026-09-07 22:36:02 +02:00
let session ;
try {
const mod = await import ( "https://silentmode.st/js/wizardconnect.js" ) ;
session = await mod . openWizardSession ( {
dappName : BNS . REGISTRAR . wizardConnect . dappName ,
dappIcon : BNS . REGISTRAR . wizardConnect . dappIcon ,
prefix : BNS . REGISTRAR . addressPrefix ,
crypto : {
pubkeyToAddress : BNS . pubkeyToAddress ,
pubkeyToLockingBytecode : BNS . pubkeyToLockingBytecode ,
binToHex : BNS . binToHex ,
txidOfHex : BNS . txidOfHex ,
} ,
} ) ;
} catch ( e ) {
render ( ` <h3>Could not start ${ esc ( label ) } </h3><p class="sub"> ${ esc ( e . message ) } </p>
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n > < / d i v > ` ) ;
2026-09-07 23:52:12 +02:00
$ ( "back" ) . onclick = state . signInOnly ? close : stepWallet ;
2026-09-07 22:36:02 +02:00
return ;
}
const uri = session . qrUri ? ? session . uri ;
render ( `
2026-09-08 02:19:41 +02:00
$ { renderTabs ( "wc" ) }
2026-09-07 22:36:02 +02:00
< h3 > Open your wallet < / h 3 >
< p class = "sub" > Scan or paste this into $ { esc ( label ) } - capable wallet , then approve the connection . < / p >
< div class = "addr" > < code id = "uri" > $ { esc ( uri ) } < / c o d e > < b u t t o n c l a s s = " b t n g h o s t " i d = " c o p y " > C o p y < / b u t t o n > < / d i v >
< div class = "note warn" style = "margin-top:16px" > Waiting for your wallet …
The link stays valid until you close this page . < / d i v >
< div class = "row end" > < button class = "btn ghost" id = "cancel" > Cancel < / b u t t o n > < / d i v >
` );
$ ( "copy" ) . onclick = ( ) => navigator . clipboard ? . writeText ( uri ) ;
2026-09-07 23:52:12 +02:00
$ ( "cancel" ) . onclick = async ( ) => { try { await session . disconnect ( ) ; } catch { } ( state . signInOnly ? close : stepWallet ) ( ) ; } ;
2026-09-07 22:36:02 +02:00
try { await session . ready ; } catch ( e ) { err ( e ) ; return ; }
2026-09-07 23:52:12 +02:00
// Sign-in-only path: get the address and persist siriusProfile immediately,
// skip the whole "quote registration" branch since there is no name.
if ( state . signInOnly ) {
try {
const addresses = await session . getAddresses ( ) ;
if ( ! addresses ? . length ) throw new Error ( "wallet returned no addresses" ) ;
state . session = session ; // keep alive for record edits etc.
finishSignIn ( {
source : "wc" ,
address : addresses [ 0 ] ,
tokenAddress : BNS . toTokenAddress ( addresses [ 0 ] ) ,
watchedAddresses : addresses ,
} ) ;
} catch ( e ) { err ( e ) ; }
return ;
}
2026-09-07 22:36:02 +02:00
stepExternalConfirm ( session , label ) ;
}
async function stepExternalConfirm ( session , label ) {
render ( ` <h3>Connected</h3><p class="sub">Reading your wallet and building the registration…</p> ` ) ;
try {
const c = await client ( ) ;
const addresses = await session . getAddresses ( ) ;
const owner = addresses [ 0 ] ;
const balance = await BNS . getBalance ( c , addresses ) ;
const genesis = BNS . describeGenesisRequirement ( balance . hasGenesisInput ) ;
if ( balance . sats === 0 n ) {
render ( ` <h3>That wallet is empty</h3>
< p class = "sub" > Fund < span class = "mono" > $ { esc ( owner ) } < / s p a n > w i t h c h i p n e t c o i n s a n d r e c o n n e c t . < / p >
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n > < / d i v > ` ) ;
$ ( "back" ) . onclick = async ( ) => { try { await session . disconnect ( ) ; } catch { } stepWallet ( ) ; } ;
return ;
}
2026-09-08 00:10:11 +02:00
// TLD external path: keep the session, hand off to the TLD confirm/mint
// steps which know to call registerTldWithExternalWallet.
if ( state . tld ) { state . session = session ; return stepConfirmTld ( ) ; }
2026-09-09 01:09:05 +02:00
const feeOverride = state . nameFeeSats != null
? { serviceFee : { ... ( BNS . REGISTRAR ? . serviceFee ? ? { } ) , sats : state . nameFeeSats } }
: { } ;
const quote = await BNS . quoteRegistration ( c , { name : state . name , ownerAddress : owner , records : { } , ... feeOverride } ) ;
2026-09-07 22:36:02 +02:00
const rows = BNS . priceSummary ( quote . costs ) . map ( ( l ) => `
< tr class = "${l.total ? " total " : " "}" > < td > $ { esc ( l . label ) } $ { l . note ? ` <small> ${ esc ( l . note ) } </small> ` : "" } < / t d >
< td > $ { sats ( l . sats ) } sat < / t d > < / t r > ` ) . j o i n ( " " ) ;
render ( `
< h3 > Confirm $ { esc ( quote . displayName ) } < / h 3 >
< p class = "sub" > Connected to $ { esc ( label ) } . The certificate is minted to
< span class = "mono" > $ { esc ( owner ) } < / s p a n > — y o u r w a l l e t , y o u r k e y . < / p >
< table class = "price" > $ { rows } < / t a b l e >
$ { genesis . ready ? "" : ` <div class="note warn" style="margin-top:16px">
< b > Two approvals needed . < / b > $ { e s c ( g e n e s i s . e x p l a n a t i o n ) } < / d i v > ` }
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "go" > Register — approve in your wallet < / b u t t o n > < / d i v >
` );
$ ( "back" ) . onclick = async ( ) => { try { await session . disconnect ( ) ; } catch { } stepWallet ( ) ; } ;
$ ( "go" ) . onclick = async ( ) => {
render ( ` <h3>Approve in your wallet</h3>
< p class = "sub" > $ { esc ( label ) } has sent the transaction to your device . Check the details there
and approve . Do not close this tab . < / p > < d i v c l a s s = " s t e p s - l o g " i d = " l o g " > < / d i v > ` ) ;
const log = $ ( "log" ) ;
const say = ( s ) => { const d = document . createElement ( "div" ) ; d . textContent = s ; log . appendChild ( d ) ; } ;
try {
const res = await BNS . registerWithExternalWallet ( await client ( ) , {
2026-09-09 01:09:05 +02:00
session , name : state . name , ownerAddress : owner , records : { } , onProgress : say , ... feeOverride ,
2026-09-07 22:36:02 +02:00
} ) ;
state . result = res ;
state . session = session ;
stepDone ( ) ;
} catch ( e ) {
err ( e ) ;
const row = document . createElement ( "div" ) ; row . className = "row end" ;
row . innerHTML = ` <button class="btn ghost" id="back">Back</button> ` ;
sheet . appendChild ( row ) ;
$ ( "back" ) . onclick = ( ) => stepExternalConfirm ( session , label ) ;
}
} ;
} catch ( e ) { err ( e ) ; }
}
// ---------- built-in: create ----------
function stepCreate ( ) {
2026-09-07 23:52:12 +02:00
// The Register button flow lands here via stepWallet, so "Back" returns to
// the wallet-choice step. The sign-in dropdown calls stepCreate directly
// with no chosen name, so "Back" should just close the modal in that case.
const goBack = state . signInOnly ? close : stepWallet ;
2026-09-07 22:36:02 +02:00
render ( `
2026-09-08 02:19:41 +02:00
$ { renderTabs ( "new" ) }
2026-09-07 22:36:02 +02:00
< h3 > Create your wallet < / h 3 >
< p class = "sub" > Generated in this browser . The phrase is never sent anywhere — not to us , not to anyone . < / p >
< label for = "pw" > Password ( encrypts the wallet on this device ) < / l a b e l >
< input type = "password" id = "pw" autocomplete = "new-password" placeholder = "at least 8 characters" >
< label for = "pw2" > Repeat password < / l a b e l >
< input type = "password" id = "pw2" autocomplete = "new-password" >
< div class = "note warn" style = "margin-top:16px" > < b > There is no reset . < / b > T h e p a s s w o r d p r o t e c t s t h i s
device ; the recovery phrase you will see next < i > is < / i > t h e w a l l e t . L o s e b o t h a n d t h e n a m e i s g o n e
forever — that is what self - custody means . < / d i v >
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "next" > Generate phrase → < / b u t t o n > < / d i v >
` );
2026-09-07 23:52:12 +02:00
$ ( "back" ) . onclick = goBack ;
2026-09-07 22:36:02 +02:00
$ ( "next" ) . onclick = async ( ) => {
const pw = $ ( "pw" ) . value , pw2 = $ ( "pw2" ) . value ;
if ( pw . length < 8 ) return err ( new Error ( "password must be at least 8 characters" ) ) ;
if ( pw !== pw2 ) return err ( new Error ( "passwords do not match" ) ) ;
try {
const wallet = await BNS . BuiltInWallet . create ( ) ;
await wallet . save ( pw ) ;
state . wallet = wallet ;
stepPhrase ( ) ;
} catch ( e ) { err ( e ) ; }
} ;
}
function stepPhrase ( ) {
const words = state . wallet . mnemonic . split ( " " ) ;
render ( `
< h3 > Write this down . Now . < / h 3 >
< p class = "sub" > These 12 words are your wallet . Anyone who has them owns your names ; if you lose them ,
nobody — including us — can recover anything . < / p >
< div class = "phrase" > $ { words . map ( ( w , i ) => ` <span><b> ${ i + 1 } </b> ${ esc ( w ) } </span> ` ) . join ( "" ) } < / d i v >
< div class = "row" > < button class = "btn ghost" id = "copy" > Copy phrase < / b u t t o n > < / d i v >
< label class = "chk" > < input type = "checkbox" id = "ack" >
< span > I have written the phrase down somewhere safe and offline . I understand it cannot be reset . < / s p a n > < / l a b e l >
< div class = "row end" > < button class = "btn acid" id = "next" disabled > Continue → < / b u t t o n > < / d i v >
` );
$ ( "copy" ) . onclick = ( ) => navigator . clipboard ? . writeText ( state . wallet . mnemonic ) ;
$ ( "ack" ) . onchange = ( e ) => { $ ( "next" ) . disabled = ! e . target . checked ; } ;
feat(sirius-x/signin): PIN escrow — encrypted-at-rest quick unlock, 3-strike password fallback
Removes the sessionStorage plaintext-mnemonic cache (fixed under the
same commit) and replaces it with a PIN-encrypted blob in localStorage.
No plaintext secret ever touches disk or memory outside the live
BuiltInWallet object.
New js/pin-escrow.js — WebCrypto PBKDF2(50k) + AES-GCM(256). Public
API on window.siriusPin: savePinBlob(mnemonic, pin), tryUnlock(pin),
hasPin(), attemptsUsed(), attemptsRemaining(), clear(), MAX_ATTEMPTS.
Iteration count is lighter than BuiltInWallet's 250k because a 4-6
digit PIN's key space is small anyway; the point is 'not plaintext at
rest,' not brute-force resistance — the durable secret is the full
password.
register-flow.js:
- After a fresh password unlock (Import / Create / Unlock), stepSetPin
offers a 4-6 digit PIN with confirm — skippable with 'Not now'.
Never overwrites an existing PIN blob.
- stepUnlock now shows a numeric PIN pad when a PIN blob is present;
the password field only appears when the user opts to 'Use password
instead' or after the blob was wiped.
- Wrong PIN → increment counter, surface 'N attempts left'. Third
wrong PIN → wipe blob and route to a 'PIN reset' screen that hands
off to the password form.
- Correct PIN → decrypt the mnemonic in-browser, rebuild the
BuiltInWallet, finishSignIn(). Attempt counter resets to 0.
profile-menu.js:
- Sign-out clears the PIN blob (via siriusPin.clear()) alongside the
siriusProfile so the device isn't quick-unlockable with a stale PIN.
All pages that host the sign-in flow now include pin-escrow.js. Same
tabbed layout in stepUnlock — the PIN pad and the password field both
live under 🔓 Unlock, transparent tab-switch works exactly as before.
Verified live:
- Fresh Import → 'Set a PIN' step → 4242 confirmed → blob written
- Reload → PIN pad, 3 attempts remaining
- Correct PIN 4242 → signed in, counter resets to 0
- 3 wrong PIN attempts → 'Wrong PIN — N attempts left' per attempt,
then 'PIN locked — enter your full password to continue', blob
wiped, next reload shows the password form
- localStorage contains only ciphertext + salt + iv + counter; no
plaintext mnemonic anywhere on disk or in sessionStorage.
2026-09-09 01:31:18 +02:00
$ ( "next" ) . onclick = ( ) => stepSetPin ( state . wallet , ( ) => { if ( ! finishSignIn ( state . wallet ) ) stepFund ( ) ; } ) ;
2026-09-07 22:36:02 +02:00
}
// ---------- built-in: import / unlock ----------
function stepImport ( ) {
2026-09-07 23:52:12 +02:00
const goBack = state . signInOnly ? close : stepWallet ;
2026-09-07 22:36:02 +02:00
render ( `
2026-09-08 02:19:41 +02:00
$ { renderTabs ( "import" ) }
2026-09-07 22:36:02 +02:00
< h3 > Import a recovery phrase < / h 3 >
< p class = "sub" > 12 words , separated by spaces . It stays in this browser . < / p >
< label for = "mn" > Recovery phrase < / l a b e l >
< textarea id = "mn" spellcheck = "false" placeholder = "word word word …" > < / t e x t a r e a >
< label for = "pw" > Password to encrypt it on this device < / l a b e l >
< input type = "password" id = "pw" autocomplete = "new-password" >
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "next" > Import → < / b u t t o n > < / d i v >
` );
2026-09-07 23:52:12 +02:00
$ ( "back" ) . onclick = goBack ;
2026-09-07 22:36:02 +02:00
$ ( "next" ) . onclick = async ( ) => {
try {
if ( $ ( "pw" ) . value . length < 8 ) throw new Error ( "password must be at least 8 characters" ) ;
const wallet = await BNS . BuiltInWallet . fromMnemonic ( $ ( "mn" ) . value ) ;
await wallet . save ( $ ( "pw" ) . value ) ;
state . wallet = wallet ;
feat(sirius-x/signin): PIN escrow — encrypted-at-rest quick unlock, 3-strike password fallback
Removes the sessionStorage plaintext-mnemonic cache (fixed under the
same commit) and replaces it with a PIN-encrypted blob in localStorage.
No plaintext secret ever touches disk or memory outside the live
BuiltInWallet object.
New js/pin-escrow.js — WebCrypto PBKDF2(50k) + AES-GCM(256). Public
API on window.siriusPin: savePinBlob(mnemonic, pin), tryUnlock(pin),
hasPin(), attemptsUsed(), attemptsRemaining(), clear(), MAX_ATTEMPTS.
Iteration count is lighter than BuiltInWallet's 250k because a 4-6
digit PIN's key space is small anyway; the point is 'not plaintext at
rest,' not brute-force resistance — the durable secret is the full
password.
register-flow.js:
- After a fresh password unlock (Import / Create / Unlock), stepSetPin
offers a 4-6 digit PIN with confirm — skippable with 'Not now'.
Never overwrites an existing PIN blob.
- stepUnlock now shows a numeric PIN pad when a PIN blob is present;
the password field only appears when the user opts to 'Use password
instead' or after the blob was wiped.
- Wrong PIN → increment counter, surface 'N attempts left'. Third
wrong PIN → wipe blob and route to a 'PIN reset' screen that hands
off to the password form.
- Correct PIN → decrypt the mnemonic in-browser, rebuild the
BuiltInWallet, finishSignIn(). Attempt counter resets to 0.
profile-menu.js:
- Sign-out clears the PIN blob (via siriusPin.clear()) alongside the
siriusProfile so the device isn't quick-unlockable with a stale PIN.
All pages that host the sign-in flow now include pin-escrow.js. Same
tabbed layout in stepUnlock — the PIN pad and the password field both
live under 🔓 Unlock, transparent tab-switch works exactly as before.
Verified live:
- Fresh Import → 'Set a PIN' step → 4242 confirmed → blob written
- Reload → PIN pad, 3 attempts remaining
- Correct PIN 4242 → signed in, counter resets to 0
- 3 wrong PIN attempts → 'Wrong PIN — N attempts left' per attempt,
then 'PIN locked — enter your full password to continue', blob
wiped, next reload shows the password form
- localStorage contains only ciphertext + salt + iv + counter; no
plaintext mnemonic anywhere on disk or in sessionStorage.
2026-09-09 01:31:18 +02:00
stepSetPin ( wallet , ( ) => { if ( ! finishSignIn ( wallet ) ) stepFund ( ) ; } ) ;
2026-09-07 22:36:02 +02:00
} catch ( e ) { err ( e ) ; }
} ;
}
function stepUnlock ( ) {
2026-09-07 23:52:12 +02:00
const goBack = state . signInOnly ? close : stepWallet ;
2026-09-09 00:47:32 +02:00
const saved = BNS . BuiltInWallet . exists ( ) ;
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
// A persistent device session signs in without any prompt.
if ( ! state . wallet && window . siriusSession ? . hasSession ? . ( ) ) {
render ( ` ${ renderTabs ( "unlock" ) } <h3>Signing you in…</h3><p class="sub">Restoring your session on this device.</p> ` ) ;
restoreSessionWallet ( ) . then ( ( w ) => {
if ( w ) { state . wallet = w ; if ( ! finishSignIn ( w ) ) stepFund ( ) ; }
else stepUnlock ( ) ;
} ) ;
return ;
}
2026-09-09 00:47:32 +02:00
// No saved wallet on this device — show why unlock isn't possible and
// route the user to Create/Import via the tab bar (which stays visible).
if ( ! saved ) {
render ( `
$ { renderTabs ( "unlock" ) }
< h3 > No wallet on this device < / h 3 >
< p class = "sub" > Unlock only works after you have created or imported a wallet on this browser .
Either < b > Create < / b > a f r e s h o n e ( w r i t e s a n e n c r y p t e d c o p y h e r e y o u c a n u n l o c k n e x t t i m e )
or < b > Import < / b > a r e c o v e r y p h r a s e y o u a l r e a d y h a v e . < / p >
< div class = "row end" >
< button class = "btn ghost" id = "go-import" type = "button" > 📥 Import a wallet → < / b u t t o n >
< button class = "btn acid" id = "go-new" type = "button" > 🆕 Create a wallet → < / b u t t o n >
< / d i v >
` );
$ ( "go-import" ) . onclick = stepImport ;
$ ( "go-new" ) . onclick = stepCreate ;
return ;
}
feat(sirius-x/signin): PIN escrow — encrypted-at-rest quick unlock, 3-strike password fallback
Removes the sessionStorage plaintext-mnemonic cache (fixed under the
same commit) and replaces it with a PIN-encrypted blob in localStorage.
No plaintext secret ever touches disk or memory outside the live
BuiltInWallet object.
New js/pin-escrow.js — WebCrypto PBKDF2(50k) + AES-GCM(256). Public
API on window.siriusPin: savePinBlob(mnemonic, pin), tryUnlock(pin),
hasPin(), attemptsUsed(), attemptsRemaining(), clear(), MAX_ATTEMPTS.
Iteration count is lighter than BuiltInWallet's 250k because a 4-6
digit PIN's key space is small anyway; the point is 'not plaintext at
rest,' not brute-force resistance — the durable secret is the full
password.
register-flow.js:
- After a fresh password unlock (Import / Create / Unlock), stepSetPin
offers a 4-6 digit PIN with confirm — skippable with 'Not now'.
Never overwrites an existing PIN blob.
- stepUnlock now shows a numeric PIN pad when a PIN blob is present;
the password field only appears when the user opts to 'Use password
instead' or after the blob was wiped.
- Wrong PIN → increment counter, surface 'N attempts left'. Third
wrong PIN → wipe blob and route to a 'PIN reset' screen that hands
off to the password form.
- Correct PIN → decrypt the mnemonic in-browser, rebuild the
BuiltInWallet, finishSignIn(). Attempt counter resets to 0.
profile-menu.js:
- Sign-out clears the PIN blob (via siriusPin.clear()) alongside the
siriusProfile so the device isn't quick-unlockable with a stale PIN.
All pages that host the sign-in flow now include pin-escrow.js. Same
tabbed layout in stepUnlock — the PIN pad and the password field both
live under 🔓 Unlock, transparent tab-switch works exactly as before.
Verified live:
- Fresh Import → 'Set a PIN' step → 4242 confirmed → blob written
- Reload → PIN pad, 3 attempts remaining
- Correct PIN 4242 → signed in, counter resets to 0
- 3 wrong PIN attempts → 'Wrong PIN — N attempts left' per attempt,
then 'PIN locked — enter your full password to continue', blob
wiped, next reload shows the password form
- localStorage contains only ciphertext + salt + iv + counter; no
plaintext mnemonic anywhere on disk or in sessionStorage.
2026-09-09 01:31:18 +02:00
// PIN escrow — if the user set a PIN on a previous session, prefer that.
// 3 wrong PINs wipe the blob and this branch falls back to the password
// form on next render.
if ( window . siriusPin ? . hasPin ? . ( ) ) {
const remaining = window . siriusPin . attemptsRemaining ( ) ;
render ( `
$ { renderTabs ( "unlock" ) }
< h3 > Enter your PIN < / h 3 >
< p class = "sub" > 4 – 6 digits . Set on this device to skip typing your full password .
$ { remaining < 3 ? ` <b class="err" style="color:var(--taken,#f6768a)"> ${ esc ( String ( remaining ) ) } attempt ${ remaining === 1 ? "" : "s" } left</b> before the PIN resets and you must use your password. ` : "" } < / p >
< label for = "pin" > PIN < / l a b e l >
< input type = "password" id = "pin" inputmode = "numeric" pattern = "[0-9]*" maxlength = "6" autocomplete = "off" >
< div class = "row end" >
< button class = "btn ghost" id = "use-password" type = "button" > Use password instead < / b u t t o n >
< button class = "btn acid" id = "next" type = "button" > Unlock → < / b u t t o n >
< / d i v >
` );
$ ( "use-password" ) . onclick = ( ) => { window . siriusPin . clear ( ) ; stepUnlock ( ) ; } ;
$ ( "pin" ) . focus ( ) ;
$ ( "pin" ) . onkeydown = ( e ) => { if ( e . key === "Enter" ) $ ( "next" ) . click ( ) ; } ;
$ ( "next" ) . onclick = async ( ) => {
try {
const mnemonic = await window . siriusPin . tryUnlock ( $ ( "pin" ) . value ) ;
state . wallet = await BNS . BuiltInWallet . fromMnemonic ( mnemonic ) ;
if ( ! finishSignIn ( state . wallet ) ) stepFund ( ) ;
} catch ( e ) {
if ( e . locked ) { render ( `
$ { renderTabs ( "unlock" ) }
< h3 > PIN reset < / h 3 >
< p class = "sub" > 3 wrong attempts — the PIN has been cleared . Enter your password to unlock and set a new PIN if you like . < / p >
< div class = "row end" > < button class = "btn acid" id = "go-pw" > Continue with password → < / b u t t o n > < / d i v >
` ); $ ("go-pw").onclick = stepUnlock; return; }
err ( e ) ;
}
} ;
return ;
}
2026-09-07 22:36:02 +02:00
render ( `
2026-09-08 02:19:41 +02:00
$ { renderTabs ( "unlock" ) }
2026-09-07 22:36:02 +02:00
< h3 > Unlock your wallet < / h 3 >
feat(sirius-x/signin): PIN escrow — encrypted-at-rest quick unlock, 3-strike password fallback
Removes the sessionStorage plaintext-mnemonic cache (fixed under the
same commit) and replaces it with a PIN-encrypted blob in localStorage.
No plaintext secret ever touches disk or memory outside the live
BuiltInWallet object.
New js/pin-escrow.js — WebCrypto PBKDF2(50k) + AES-GCM(256). Public
API on window.siriusPin: savePinBlob(mnemonic, pin), tryUnlock(pin),
hasPin(), attemptsUsed(), attemptsRemaining(), clear(), MAX_ATTEMPTS.
Iteration count is lighter than BuiltInWallet's 250k because a 4-6
digit PIN's key space is small anyway; the point is 'not plaintext at
rest,' not brute-force resistance — the durable secret is the full
password.
register-flow.js:
- After a fresh password unlock (Import / Create / Unlock), stepSetPin
offers a 4-6 digit PIN with confirm — skippable with 'Not now'.
Never overwrites an existing PIN blob.
- stepUnlock now shows a numeric PIN pad when a PIN blob is present;
the password field only appears when the user opts to 'Use password
instead' or after the blob was wiped.
- Wrong PIN → increment counter, surface 'N attempts left'. Third
wrong PIN → wipe blob and route to a 'PIN reset' screen that hands
off to the password form.
- Correct PIN → decrypt the mnemonic in-browser, rebuild the
BuiltInWallet, finishSignIn(). Attempt counter resets to 0.
profile-menu.js:
- Sign-out clears the PIN blob (via siriusPin.clear()) alongside the
siriusProfile so the device isn't quick-unlockable with a stale PIN.
All pages that host the sign-in flow now include pin-escrow.js. Same
tabbed layout in stepUnlock — the PIN pad and the password field both
live under 🔓 Unlock, transparent tab-switch works exactly as before.
Verified live:
- Fresh Import → 'Set a PIN' step → 4242 confirmed → blob written
- Reload → PIN pad, 3 attempts remaining
- Correct PIN 4242 → signed in, counter resets to 0
- 3 wrong PIN attempts → 'Wrong PIN — N attempts left' per attempt,
then 'PIN locked — enter your full password to continue', blob
wiped, next reload shows the password form
- localStorage contains only ciphertext + salt + iv + counter; no
plaintext mnemonic anywhere on disk or in sessionStorage.
2026-09-09 01:31:18 +02:00
< p class = "sub" > Decrypts the wallet stored in this browser . After unlocking you can set a
PIN so future sessions ask for 4 – 6 digits instead of the full password . < / p >
2026-09-07 22:36:02 +02:00
< label for = "pw" > Password < / l a b e l >
< input type = "password" id = "pw" autocomplete = "current-password" >
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "next" > Unlock → < / b u t t o n > < / d i v >
` );
2026-09-07 23:52:12 +02:00
$ ( "back" ) . onclick = goBack ;
2026-09-07 22:36:02 +02:00
$ ( "pw" ) . onkeydown = ( e ) => { if ( e . key === "Enter" ) $ ( "next" ) . click ( ) ; } ;
$ ( "next" ) . onclick = async ( ) => {
2026-09-07 23:52:12 +02:00
try {
state . wallet = await BNS . BuiltInWallet . load ( $ ( "pw" ) . value ) ;
feat(sirius-x/signin): PIN escrow — encrypted-at-rest quick unlock, 3-strike password fallback
Removes the sessionStorage plaintext-mnemonic cache (fixed under the
same commit) and replaces it with a PIN-encrypted blob in localStorage.
No plaintext secret ever touches disk or memory outside the live
BuiltInWallet object.
New js/pin-escrow.js — WebCrypto PBKDF2(50k) + AES-GCM(256). Public
API on window.siriusPin: savePinBlob(mnemonic, pin), tryUnlock(pin),
hasPin(), attemptsUsed(), attemptsRemaining(), clear(), MAX_ATTEMPTS.
Iteration count is lighter than BuiltInWallet's 250k because a 4-6
digit PIN's key space is small anyway; the point is 'not plaintext at
rest,' not brute-force resistance — the durable secret is the full
password.
register-flow.js:
- After a fresh password unlock (Import / Create / Unlock), stepSetPin
offers a 4-6 digit PIN with confirm — skippable with 'Not now'.
Never overwrites an existing PIN blob.
- stepUnlock now shows a numeric PIN pad when a PIN blob is present;
the password field only appears when the user opts to 'Use password
instead' or after the blob was wiped.
- Wrong PIN → increment counter, surface 'N attempts left'. Third
wrong PIN → wipe blob and route to a 'PIN reset' screen that hands
off to the password form.
- Correct PIN → decrypt the mnemonic in-browser, rebuild the
BuiltInWallet, finishSignIn(). Attempt counter resets to 0.
profile-menu.js:
- Sign-out clears the PIN blob (via siriusPin.clear()) alongside the
siriusProfile so the device isn't quick-unlockable with a stale PIN.
All pages that host the sign-in flow now include pin-escrow.js. Same
tabbed layout in stepUnlock — the PIN pad and the password field both
live under 🔓 Unlock, transparent tab-switch works exactly as before.
Verified live:
- Fresh Import → 'Set a PIN' step → 4242 confirmed → blob written
- Reload → PIN pad, 3 attempts remaining
- Correct PIN 4242 → signed in, counter resets to 0
- 3 wrong PIN attempts → 'Wrong PIN — N attempts left' per attempt,
then 'PIN locked — enter your full password to continue', blob
wiped, next reload shows the password form
- localStorage contains only ciphertext + salt + iv + counter; no
plaintext mnemonic anywhere on disk or in sessionStorage.
2026-09-09 01:31:18 +02:00
// Fresh password unlock — offer the PIN shortcut before finishing.
stepSetPin ( state . wallet , ( ) => { if ( ! finishSignIn ( state . wallet ) ) stepFund ( ) ; } ) ;
2026-09-07 23:52:12 +02:00
} catch ( e ) { err ( e ) ; }
2026-09-07 22:36:02 +02:00
} ;
}
feat(sirius-x/signin): PIN escrow — encrypted-at-rest quick unlock, 3-strike password fallback
Removes the sessionStorage plaintext-mnemonic cache (fixed under the
same commit) and replaces it with a PIN-encrypted blob in localStorage.
No plaintext secret ever touches disk or memory outside the live
BuiltInWallet object.
New js/pin-escrow.js — WebCrypto PBKDF2(50k) + AES-GCM(256). Public
API on window.siriusPin: savePinBlob(mnemonic, pin), tryUnlock(pin),
hasPin(), attemptsUsed(), attemptsRemaining(), clear(), MAX_ATTEMPTS.
Iteration count is lighter than BuiltInWallet's 250k because a 4-6
digit PIN's key space is small anyway; the point is 'not plaintext at
rest,' not brute-force resistance — the durable secret is the full
password.
register-flow.js:
- After a fresh password unlock (Import / Create / Unlock), stepSetPin
offers a 4-6 digit PIN with confirm — skippable with 'Not now'.
Never overwrites an existing PIN blob.
- stepUnlock now shows a numeric PIN pad when a PIN blob is present;
the password field only appears when the user opts to 'Use password
instead' or after the blob was wiped.
- Wrong PIN → increment counter, surface 'N attempts left'. Third
wrong PIN → wipe blob and route to a 'PIN reset' screen that hands
off to the password form.
- Correct PIN → decrypt the mnemonic in-browser, rebuild the
BuiltInWallet, finishSignIn(). Attempt counter resets to 0.
profile-menu.js:
- Sign-out clears the PIN blob (via siriusPin.clear()) alongside the
siriusProfile so the device isn't quick-unlockable with a stale PIN.
All pages that host the sign-in flow now include pin-escrow.js. Same
tabbed layout in stepUnlock — the PIN pad and the password field both
live under 🔓 Unlock, transparent tab-switch works exactly as before.
Verified live:
- Fresh Import → 'Set a PIN' step → 4242 confirmed → blob written
- Reload → PIN pad, 3 attempts remaining
- Correct PIN 4242 → signed in, counter resets to 0
- 3 wrong PIN attempts → 'Wrong PIN — N attempts left' per attempt,
then 'PIN locked — enter your full password to continue', blob
wiped, next reload shows the password form
- localStorage contains only ciphertext + salt + iv + counter; no
plaintext mnemonic anywhere on disk or in sessionStorage.
2026-09-09 01:31:18 +02:00
// After a successful password sign-in (Import / Create / Unlock), offer to
// stash the mnemonic under a PIN so next session opens with a numeric pad.
// User can decline ("Not now"); either way we call `next` afterwards.
function stepSetPin ( w , next ) {
if ( ! window . siriusPin || ! w ? . mnemonic ) return next ( ) ;
// Already have a PIN blob? Don't overwrite silently — skip.
if ( window . siriusPin . hasPin ( ) ) return next ( ) ;
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
// With "stay signed in" on (the default) the device session already
// makes the next visit prompt-free; a PIN would just be one more thing
// to type. It stays available in the dashboard Settings for people who
// want a gate before payments.
if ( window . siriusSession ? . isEnabled ? . ( ) ) return next ( ) ;
feat(sirius-x/signin): PIN escrow — encrypted-at-rest quick unlock, 3-strike password fallback
Removes the sessionStorage plaintext-mnemonic cache (fixed under the
same commit) and replaces it with a PIN-encrypted blob in localStorage.
No plaintext secret ever touches disk or memory outside the live
BuiltInWallet object.
New js/pin-escrow.js — WebCrypto PBKDF2(50k) + AES-GCM(256). Public
API on window.siriusPin: savePinBlob(mnemonic, pin), tryUnlock(pin),
hasPin(), attemptsUsed(), attemptsRemaining(), clear(), MAX_ATTEMPTS.
Iteration count is lighter than BuiltInWallet's 250k because a 4-6
digit PIN's key space is small anyway; the point is 'not plaintext at
rest,' not brute-force resistance — the durable secret is the full
password.
register-flow.js:
- After a fresh password unlock (Import / Create / Unlock), stepSetPin
offers a 4-6 digit PIN with confirm — skippable with 'Not now'.
Never overwrites an existing PIN blob.
- stepUnlock now shows a numeric PIN pad when a PIN blob is present;
the password field only appears when the user opts to 'Use password
instead' or after the blob was wiped.
- Wrong PIN → increment counter, surface 'N attempts left'. Third
wrong PIN → wipe blob and route to a 'PIN reset' screen that hands
off to the password form.
- Correct PIN → decrypt the mnemonic in-browser, rebuild the
BuiltInWallet, finishSignIn(). Attempt counter resets to 0.
profile-menu.js:
- Sign-out clears the PIN blob (via siriusPin.clear()) alongside the
siriusProfile so the device isn't quick-unlockable with a stale PIN.
All pages that host the sign-in flow now include pin-escrow.js. Same
tabbed layout in stepUnlock — the PIN pad and the password field both
live under 🔓 Unlock, transparent tab-switch works exactly as before.
Verified live:
- Fresh Import → 'Set a PIN' step → 4242 confirmed → blob written
- Reload → PIN pad, 3 attempts remaining
- Correct PIN 4242 → signed in, counter resets to 0
- 3 wrong PIN attempts → 'Wrong PIN — N attempts left' per attempt,
then 'PIN locked — enter your full password to continue', blob
wiped, next reload shows the password form
- localStorage contains only ciphertext + salt + iv + counter; no
plaintext mnemonic anywhere on disk or in sessionStorage.
2026-09-09 01:31:18 +02:00
render ( `
$ { renderTabs ( "unlock" ) }
< h3 > Set a PIN < span class = "pill" > optional < / s p a n > < / h 3 >
< p class = "sub" > 4 – 6 digits . Encrypts your recovery phrase under this PIN so future sessions on
this device unlock with just the PIN . Three wrong PINs and the PIN is cleared — you ' d fall
back to your full password . < / p >
< label for = "pin1" > PIN < / l a b e l >
< input type = "password" id = "pin1" inputmode = "numeric" pattern = "[0-9]*" maxlength = "6" autocomplete = "off" >
< label for = "pin2" style = "margin-top:12px" > Repeat PIN < / l a b e l >
< input type = "password" id = "pin2" inputmode = "numeric" pattern = "[0-9]*" maxlength = "6" autocomplete = "off" >
< div class = "row end" >
< button class = "btn ghost" id = "skip" type = "button" > Not now < / b u t t o n >
< button class = "btn acid" id = "save-pin" type = "button" > Save PIN → < / b u t t o n >
< / d i v >
` );
$ ( "skip" ) . onclick = next ;
$ ( "pin2" ) . onkeydown = ( e ) => { if ( e . key === "Enter" ) $ ( "save-pin" ) . click ( ) ; } ;
$ ( "save-pin" ) . onclick = async ( ) => {
const a = $ ( "pin1" ) . value , b = $ ( "pin2" ) . value ;
if ( ! /^\d{4,6}$/ . test ( a ) ) return err ( new Error ( "PIN must be 4– 6 digits" ) ) ;
if ( a !== b ) return err ( new Error ( "PINs do not match" ) ) ;
try { await window . siriusPin . savePinBlob ( w . mnemonic , a ) ; next ( ) ; }
catch ( e ) { err ( e ) ; }
} ;
}
2026-09-07 22:36:02 +02:00
// ---------- fund ----------
let fundTimer = null ;
async function stepFund ( ) {
const w = state . wallet ;
const faucets = BNS . REGISTRAR . faucets
. map ( ( f ) => ` <a href=" ${ esc ( f . url ) } " target="_blank" rel="noopener" style="color:var(--acid,#d6ff3d)"> ${ esc ( f . name ) } </a> ` ) . join ( " · " ) ;
render ( `
< h3 > Fund your wallet < / h 3 >
< p class = "sub" > Send test coins to this address . It is yours — derived from your phrase . < / p >
< div class = "addr" > < code id = "a" > $ { esc ( w . address ) } < / c o d e >
< button class = "btn ghost" id = "copy" > Copy < / b u t t o n > < / d i v >
< div class = "muted" style = "margin-top:10px" > Free chipnet coins : $ { faucets } < / d i v >
< div class = "note" id = "bal" style = "margin-top:16px" > checking balance … < / d i v >
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "next" disabled > Continue → < / b u t t o n > < / d i v >
` );
$ ( "copy" ) . onclick = ( ) => navigator . clipboard ? . writeText ( w . address ) ;
$ ( "back" ) . onclick = ( ) => { clearInterval ( fundTimer ) ; stepWallet ( ) ; } ;
$ ( "next" ) . onclick = ( ) => { clearInterval ( fundTimer ) ; stepConfirm ( ) ; } ;
const poll = async ( ) => {
try {
const b = await BNS . getBalance ( await client ( ) , w . watchedAddresses ) ;
const enough = b . sats >= 20000 n ;
$ ( "bal" ) . innerHTML = b . sats > 0 n
? ` <b class="ok"> ${ sats ( b . sats ) } sat</b> available ( ${ b . coins } coin ${ b . coins === 1 ? "" : "s" } ) `
+ ( enough ? "" : ` — a little more is needed to cover the registration. ` )
: ` Waiting for coins… this page is watching the chain live. ` ;
$ ( "next" ) . disabled = ! enough ;
} catch ( e ) { $ ( "bal" ) . textContent = "could not reach the chain: " + e . message ; }
} ;
await poll ( ) ;
clearInterval ( fundTimer ) ;
fundTimer = setInterval ( poll , 6000 ) ;
}
// ---------- confirm ----------
async function stepConfirm ( ) {
render ( ` <h3>Preparing…</h3><p class="sub">Building your registration transaction.</p> ` ) ;
try {
const c = await client ( ) ;
2026-09-08 00:10:11 +02:00
// TLD flow skips the name-availability check and the per-name quote —
// registerTldWith*Wallet builds the tx internally and checks the on-chain
// TLD registry itself. Just show a simple confirm screen with the fee.
if ( state . tld ) return stepConfirmTld ( ) ;
2026-09-07 22:36:02 +02:00
const avail = await BNS . checkAvailability ( c , state . name ) ;
if ( ! avail . available ) throw new Error ( ` " ${ avail . name } " was just registered by someone else ` ) ;
2026-09-09 01:09:05 +02:00
const feeOverride = state . nameFeeSats != null
? { serviceFee : { ... ( BNS . REGISTRAR ? . serviceFee ? ? { } ) , sats : state . nameFeeSats } }
: { } ;
2026-09-07 22:36:02 +02:00
const quote = await BNS . quoteRegistration ( c , {
2026-09-09 01:09:05 +02:00
name : state . name , ownerAddress : state . wallet . address , records : { } , ... feeOverride ,
2026-09-07 22:36:02 +02:00
} ) ;
state . quote = quote ;
2026-09-09 01:09:05 +02:00
state . _feeOverride = feeOverride ;
2026-09-07 22:36:02 +02:00
const rows = BNS . priceSummary ( quote . costs ) . map ( ( l ) => `
< tr class = "${l.total ? " total " : " "}" > < td > $ { esc ( l . label ) } $ { l . note ? ` <small> ${ esc ( l . note ) } </small> ` : "" } < / t d >
< td > $ { sats ( l . sats ) } sat < / t d > < / t r > ` ) . j o i n ( " " ) ;
render ( `
< h3 > Confirm $ { esc ( quote . displayName ) } < / h 3 >
< p class = "sub" > One transaction . It mints the certificate to < b > your < / b > a d d r e s s a n d p a y s t h e f e e s . < / p >
feat(sirius-x): TLD owner panel — owner-set price, on/off switch, private TLDs
A TLD owner needed to run their namespace without the operator: set what
every name under the TLD sells for, take the TLD off the public registry
for a while, and still register names under it themselves.
Both settings live on chain in the TLD's TUPD records (`price`, `hidden`)
because changes are rare and every client already walks the TLD beacon.
The gateway's /api/tlds now carries records, owner, price_usd and
hidden_by per TLD; pricing.js quotes the owner price ahead of the length
tiers and only trusts a TLD_BEACON-sourced list; the register flow refuses
hidden and frozen TLDs for the public but lets the owner through at the
platform share only (the 90% owner cut would be paid to themselves). The
portal's TLDs tab loads real holdings, shows price and on/off state, and
gives each TLD a one-click switch, a price/policy editor and an inline
"register a name under .tld" form. Docs and the design table describe the
two new records.
2026-09-16 21:28:18 +02:00
$ { state . ownerMint ? ` <div class="note">You own this TLD, so this is an <b>owner registration</b>: only the platform's 10% share of the name price is charged — the 90% owner share would just be paid to yourself.</div> ` : "" }
2026-09-07 22:36:02 +02:00
< table class = "price" > $ { rows } < / t a b l e >
< div class = "note" style = "margin-top:16px" > The certificate will be minted to
< span class = "mono" > $ { esc ( state . wallet . address ) } < / s p a n > — y o u r k e y , y o u r n a m e .
You can set where it points immediately afterwards . < / d i v >
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "go" > Register — sign & amp ; broadcast < / b u t t o n > < / d i v >
` );
$ ( "back" ) . onclick = stepFund ;
2026-09-09 03:45:41 +02:00
$ ( "go" ) . onclick = ( ) => stepApprove ( { feeSats : state . nameFeeSats ? ? 0 n , kind : "name" , back : stepConfirm , next : stepRegister } ) ;
2026-09-07 22:36:02 +02:00
} catch ( e ) {
render ( ` <h3>Could not prepare</h3><p class="sub"> ${ esc ( e . message ) } </p>
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n > < / d i v > ` ) ;
$ ( "back" ) . onclick = stepFund ;
}
}
2026-09-08 00:10:11 +02:00
// ---------- TLD confirm ----------
async function stepConfirmTld ( ) {
const feeSats = state . tld . serviceFeeSats ;
const owner = state . wallet ? state . wallet . address : ( state . session ? ( await state . session . getAddresses ( ) ) [ 0 ] : "your address" ) ;
render ( `
< h3 > Confirm < b > $ { esc ( state . name ) } < / b > < / h 3 >
< p class = "sub" > One transaction . It mints the TLD certificate to < b > your < / b > t o k e n a d d r e s s ; y o u b e c o m e
the registry operator for every second - level name under it . < / p >
< table class = "price" >
< tr > < td > Service fee$ { feeSats === 0 n ? " <small>(none on chipnet)</small>" : "" } < / t d >
< td > $ { sats ( feeSats ) } sat < / t d > < / t r >
< tr > < td > Beacon dust + chain fee < small > ( covered by wallet ) < / s m a l l > < / t d >
< td > ~ 1 , 300 sat < / t d > < / t r >
< / t a b l e >
< div class = "note" style = "margin-top:16px" > Minted to < span class = "mono" > $ { esc ( owner ) } < / s p a n > .
That wallet becomes the operator for this TLD . < / d i v >
< div class = "row end" > < button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "go" > Register — sign & amp ; broadcast < / b u t t o n > < / d i v >
` );
$ ( "back" ) . onclick = stepFund ;
2026-09-09 03:45:41 +02:00
$ ( "go" ) . onclick = ( ) => stepApprove ( { feeSats , kind : "tld" , back : stepConfirmTld , next : stepRegisterTld } ) ;
}
// ---------- approve payment (PIN gate) ----------
// After the user has confirmed the fee and clicked "Register — sign & broadcast",
// gate the actual sign+broadcast on a fresh PIN entry (or password when no PIN
// is set on this device). The gate exists so a wallet already unlocked in
// memory still can't be spent without an explicit user gesture — same idea as
// a hardware wallet asking for its PIN on every transaction.
//
// opts: { feeSats, kind: "name"|"tld", back, next }
function stepApprove ( opts ) {
const { feeSats , kind , back , next } = opts ;
const feeLine = feeSats && feeSats > 0 n ? ` <b class="ok"> ${ sats ( feeSats ) } sat</b> ` : ` <b class="ok">Service fee waived on chipnet</b> ` ;
const target = kind === "tld" ? ` .<b> ${ esc ( state . name ) } </b> ` : ` <b> ${ esc ( state . name ) } </b> ` ;
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
// Signed in with a live wallet and no "ask for PIN before payments"
// preference: approving is one click. The signature itself is the proof;
// re-typing a secret the browser already holds adds nothing but friction.
const gateOff = ! ( window . siriusSession ? . requirePinForPayments ? . ( ) ) ;
if ( state . wallet && gateOff ) {
render ( `
< h3 > Approve payment < / h 3 >
< p class = "sub" > Sign and broadcast the registration for $ { target } . Fee : $ { feeLine } .
You can require a PIN for every payment under Settings in the dashboard . < / p >
< div class = "row end" >
< button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "approve" > Approve → < / b u t t o n >
< / d i v >
` );
$ ( "back" ) . onclick = back ;
$ ( "approve" ) . onclick = next ;
$ ( "approve" ) . focus ( ) ;
return ;
}
2026-09-09 03:45:41 +02:00
// No PIN configured — prompt for the wallet password as the approval
// gesture. (When no wallet-at-rest exists either, `state.wallet` was set
// from a fresh Create/Import, so we ask the user to set a PIN afterwards.)
const usePinPad = ! ! window . siriusPin ? . hasPin ? . ( ) ;
if ( usePinPad ) {
const remaining = window . siriusPin . attemptsRemaining ( ) ;
render ( `
< h3 > Approve payment < / h 3 >
< p class = "sub" > Enter your PIN to sign and broadcast the registration for $ { target } . Fee : $ { feeLine } .
$ { remaining < 3 ? ` <b class="err" style="color:var(--taken,#f6768a)"> ${ esc ( String ( remaining ) ) } attempt ${ remaining === 1 ? "" : "s" } left</b> before the PIN resets and you must use your password. ` : "" } < / p >
< label for = "approve-pin" > PIN < / l a b e l >
< input type = "password" id = "approve-pin" inputmode = "numeric" pattern = "[0-9]*" maxlength = "6" autocomplete = "off" >
< div class = "row end" >
< button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "approve" > Approve → < / b u t t o n >
< / d i v >
` );
$ ( "back" ) . onclick = back ;
$ ( "approve-pin" ) . focus ( ) ;
$ ( "approve-pin" ) . onkeydown = ( e ) => { if ( e . key === "Enter" ) $ ( "approve" ) . click ( ) ; } ;
$ ( "approve" ) . onclick = async ( ) => {
try {
const mnemonic = await window . siriusPin . tryUnlock ( $ ( "approve-pin" ) . value ) ;
// PIN verified — ensure `state.wallet` is loaded from the mnemonic
// (unlock counter has been reset by tryUnlock).
if ( ! state . wallet ) state . wallet = await BNS . BuiltInWallet . fromMnemonic ( mnemonic ) ;
next ( ) ;
} catch ( e ) {
if ( e . locked ) {
render ( `
< h3 > PIN reset < / h 3 >
< p class = "sub" > 3 wrong attempts — the PIN has been cleared . Enter your password to
approve the payment and set a new PIN if you like . < / p >
< div class = "row end" >
< button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "go-pw" > Continue with password → < / b u t t o n >
< / d i v >
` );
$ ( "back" ) . onclick = back ;
$ ( "go-pw" ) . onclick = ( ) => stepApprove ( opts ) ;
return ;
}
err ( e ) ;
}
} ;
return ;
}
// Fallback — no PIN blob. If we have a wallet-at-rest we ask for the
// password. If not (fresh Create/Import in this session), the wallet is
// already in memory and there is nothing to gate on locally, so proceed
// straight to sign + offer to set a PIN afterwards.
const hasWalletAtRest = ! ! ( window . localStorage && localStorage . getItem ( "bns.wallet.v1" ) ) ;
if ( ! hasWalletAtRest && state . wallet ) { next ( ) ; return ; }
render ( `
< h3 > Approve payment < / h 3 >
< p class = "sub" > Enter your wallet password to sign and broadcast the registration for
$ { target } . Fee : $ { feeLine } . Set a PIN afterwards so future payments approve with 4 – 6 digits . < / p >
< label for = "approve-pw" > Password < / l a b e l >
< input type = "password" id = "approve-pw" autocomplete = "current-password" >
< div class = "row end" >
< button class = "btn ghost" id = "back" > Back < / b u t t o n >
< button class = "btn acid" id = "approve" > Approve → < / b u t t o n >
< / d i v >
` );
$ ( "back" ) . onclick = back ;
$ ( "approve-pw" ) . focus ( ) ;
$ ( "approve-pw" ) . onkeydown = ( e ) => { if ( e . key === "Enter" ) $ ( "approve" ) . click ( ) ; } ;
$ ( "approve" ) . onclick = async ( ) => {
try {
state . wallet = await BNS . BuiltInWallet . load ( $ ( "approve-pw" ) . value ) ;
// Offer to set a PIN so the next payment prompts for digits.
stepSetPin ( state . wallet , next ) ;
} catch ( e ) { err ( e ) ; }
} ;
2026-09-08 00:10:11 +02:00
}
async function stepRegisterTld ( ) {
render ( ` <h3>Registering ${ esc ( state . name ) } </h3>
< p class = "sub" > Signing in your browser and broadcasting to the chain . Do not close this tab . < / p >
< div class = "steps-log" id = "log" > < / d i v > ` ) ;
const log = $ ( "log" ) ;
const say = ( s ) => { const d = document . createElement ( "div" ) ; d . textContent = s ; log . appendChild ( d ) ; log . scrollTop = log . scrollHeight ; } ;
const serviceFee = { address : BNS . REGISTRAR ? . serviceFee ? . address ? ? null , sats : state . tld . serviceFeeSats } ;
try {
const res = state . session
? await BNS . registerTldWithExternalWallet ( await client ( ) , {
session : state . session , tld : state . tld . label , records : { } ,
serviceFee , tldListUrl : "https://navigate.st/api/tlds" , onProgress : say ,
} )
: await BNS . registerTldWithBuiltInWallet ( await client ( ) , {
wallet : state . wallet , tld : state . tld . label , records : { } ,
serviceFee , tldListUrl : "https://navigate.st/api/tlds" , onProgress : say ,
} ) ;
state . result = res ;
stepDoneTld ( ) ;
} catch ( e ) {
err ( e ) ;
const row = document . createElement ( "div" ) ; row . className = "row end" ;
row . innerHTML = ` <button class="btn ghost" id="back">Back</button> ` ;
sheet . appendChild ( row ) ;
$ ( "back" ) . onclick = stepConfirmTld ;
}
}
function stepDoneTld ( ) {
const r = state . result ;
render ( `
< h3 > < span class = "ok" > ✓ < / s p a n > < b > . $ { e s c ( r . t l d ) } < / b > i s y o u r s < / h 3 >
< p class = "sub" > The TLD certificate is on the chain , held by your key . Second - level names under
< b > . $ { esc ( r . tld ) } < / b > n o w l i v e i n y o u r r e g i s t r y . < / p >
< table class = "price" >
< tr > < td > Transaction < / t d > < t d > $ { e s c ( r . t x i d . s l i c e ( 0 , 2 0 ) ) } … < / t d > < / t r >
< tr > < td > Certificate ID < / t d > < t d > $ { e s c ( r . c a t e g o r y . s l i c e ( 0 , 2 0 ) ) } … < / t d > < / t r >
< tr > < td > Operator < / t d > < t d > $ { e s c ( r . o w n e r A d d r e s s . s l i c e ( 0 , 2 2 ) ) } … < / t d > < / t r >
< / t a b l e >
< div class = "row end" > < button class = "btn acid" id = "fin" > Done < / b u t t o n > < / d i v >
` );
$ ( "fin" ) . onclick = close ;
}
2026-09-07 22:36:02 +02:00
// ---------- register ----------
async function stepRegister ( ) {
render ( ` <h3>Registering ${ esc ( state . name ) } </h3>
< p class = "sub" > Signing in your browser and broadcasting to the chain . Do not close this tab . < / p >
< div class = "steps-log" id = "log" > < / d i v > ` ) ;
const log = $ ( "log" ) ;
const say = ( s ) => { const d = document . createElement ( "div" ) ; d . textContent = s ; log . appendChild ( d ) ; log . scrollTop = log . scrollHeight ; } ;
const words = {
"checking-availability" : "checking the name is still free" ,
"loading-coins" : "loading your coins" ,
"preparing-wallet" : "preparing your wallet (one-off setup transaction)" ,
"prepared" : "wallet prepared" ,
"building" : "building the registration transaction" ,
"signing" : "signing with your key — in this browser" ,
"broadcasting" : "broadcasting to the Bitcoin Cash network" ,
"registered" : "registered" ,
} ;
try {
const res = await BNS . registerWithBuiltInWallet ( await client ( ) , {
wallet : state . wallet , name : state . name , records : { } ,
onProgress : ( s ) => say ( words [ s ] ? ? s ) ,
2026-09-09 01:09:05 +02:00
... ( state . _feeOverride ? ? { } ) ,
2026-09-07 22:36:02 +02:00
} ) ;
state . result = res ;
stepDone ( ) ;
} catch ( e ) {
err ( e ) ;
const row = document . createElement ( "div" ) ; row . className = "row end" ;
row . innerHTML = ` <button class="btn ghost" id="back">Back</button> ` ;
sheet . appendChild ( row ) ;
$ ( "back" ) . onclick = stepConfirm ;
}
}
// ---------- done + point somewhere ----------
function stepDone ( ) {
const r = state . result ;
render ( `
< h3 > < span class = "ok" > ✓ < / s p a n > $ { e s c ( r . d i s p l a y N a m e ) } i s y o u r s < / h 3 >
< p class = "sub" > The certificate is on the chain , held by your key . Nobody can take it back . < / p >
< table class = "price" >
< tr > < td > Transaction < / t d > < t d > $ { e s c ( r . t x i d . s l i c e ( 0 , 2 0 ) ) } … < / t d > < / t r >
< tr > < td > Certificate ID < / t d > < t d > $ { e s c ( r . c a t e g o r y . s l i c e ( 0 , 2 0 ) ) } … < / t d > < / t r >
< tr > < td > Owner < / t d > < t d > $ { e s c ( r . o w n e r A d d r e s s . s l i c e ( 0 , 2 2 ) ) } … < / t d > < / t r >
< / t a b l e >
< h3 style = "margin-top:24px;font-size:1.05rem" > Point it somewhere < / h 3 >
< p class = "sub" > Optional , and changeable any time — only your key can . < / p >
< label for = "kind" > Record type < / l a b e l >
< select id = "kind" >
< option value = "h" > A tiny site stored on the chain itself ( h ) < / o p t i o n >
< option value = "u" > Redirect to a web address ( u ) < / o p t i o n >
< option value = "ip" > Your own server ' s IPv4 address ( ip ) < / o p t i o n >
< option value = "tls" > TLS certificate fingerprint ( tls ) < / o p t i o n >
< / s e l e c t >
< label for = "val" id = "vl" > HTML — kept small ; it lives inside the transaction < / l a b e l >
< textarea id = "val" spellcheck = "false" placeholder = "<h1>hello</h1>" > < / t e x t a r e a >
< div class = "muted" id = "budget" > < / d i v >
< div class = "row end" > < button class = "btn ghost" id = "skip" > Done for now < / b u t t o n >
< button class = "btn acid" id = "set" > Publish record < / b u t t o n > < / d i v >
` );
const labels = {
h : [ "HTML — kept small; it lives inside the transaction" , "<h1>hello</h1>" ] ,
u : [ "The https:// address the name should open" , "https://example.org" ] ,
ip : [ "IPv4 address of your server" , "203.0.113.9" ] ,
tls : [ "SHA-256 fingerprint of your TLS certificate (hex)" , "a1b2c3…" ] ,
} ;
const budget = ( ) => {
const used = new TextEncoder ( ) . encode ( $ ( "val" ) . value ) . length ;
const max = 200 - 30 - r . name . length ;
$ ( "budget" ) . textContent = ` ${ used } / ~ ${ max } bytes used ` ;
$ ( "budget" ) . style . color = used > max ? "var(--taken,#f6768a)" : "" ;
} ;
$ ( "kind" ) . onchange = ( ) => { const [ l , p ] = labels [ $ ( "kind" ) . value ] ; $ ( "vl" ) . textContent = l ; $ ( "val" ) . placeholder = p ; budget ( ) ; } ;
$ ( "val" ) . oninput = budget ;
budget ( ) ;
$ ( "skip" ) . onclick = close ;
$ ( "set" ) . onclick = async ( ) => {
const records = { [ $ ( "kind" ) . value ] : $ ( "val" ) . value } ;
const external = ! state . wallet && state . session ;
render ( ` <h3>Publishing record</h3><p class="sub">This moves the certificate — which is how the chain
proves you are the owner . $ { external ? " Approve it in your wallet." : "" } < / p >
< div class = "steps-log" id = "log" > < / d i v > ` ) ;
const log = $ ( "log" ) ;
const say = ( s ) => { const d = document . createElement ( "div" ) ; d . textContent = s ; log . appendChild ( d ) ; } ;
try {
const upd = external
? await BNS . setRecordsWithExternalWallet ( await client ( ) , {
session : state . session , name : r . displayName , records , onProgress : say ,
} )
: await BNS . setRecordsWithBuiltInWallet ( await client ( ) , {
wallet : state . wallet , name : r . displayName , records , onProgress : say ,
} ) ;
render ( ` <h3><span class="ok">✓</span> ${ esc ( r . displayName ) } now points somewhere</h3>
< p class = "sub" > Update transaction < span class = "mono" > $ { esc ( upd . txid . slice ( 0 , 20 ) ) } … < / s p a n > i s o n t h e c h a i n .
Resolvers pick it up within a block . < / p >
< div class = "note" > Open it with the Silent Mode resolver , or preview it at
< a href = "https://navigate.st/bns/${encodeURIComponent(r.displayName)}" target = "_blank" rel = "noopener" style = "color:var(--acid,#d6ff3d)" > navigate . st < / a > . < / d i v >
< div class = "row end" > < button class = "btn acid" id = "fin" > Finish < / b u t t o n > < / d i v > ` ) ;
$ ( "fin" ) . onclick = close ;
} catch ( e ) {
err ( e ) ;
const row = document . createElement ( "div" ) ; row . className = "row end" ;
row . innerHTML = ` <button class="btn ghost" id="back">Back</button> ` ;
sheet . appendChild ( row ) ; $ ( "back" ) . onclick = stepDone ;
}
} ;
}