After a successful USD listing the panel tried to show the BCH-style offer
hex, which dollar listings do not have, so the seller saw an error and a
bare Cancel button. The panel now shows the contract address instead.
The gateway pruned listings from its store whenever one electrum lookup did
not show the covenant output. A listing a few seconds old can be invisible
to one server while another already relayed it; deleting it then loses a
real on-chain listing. Absence is now trusted only after 20 minutes; before
that the listing is hidden from buyers but kept.
Everything a site needs past the first screen — an about page, a blog, a
contact page — had to live on that one page or not exist. The folder on Sia
could always hold more; the studio could only ever edit index.html.
Pages are now a list. Each one is a folder with its own index.html, so it
answers to the address a visitor would guess (/about/), and the sidebar
shows that address rather than the file behind it. The title is editable and
the address is not: a published URL that moves is a link somebody else wrote
down and can no longer follow.
Draft and Publish both walk the whole list. Pages that link to each other
are only coherent if they go up together, so there is one button and it
sends everything, writes a sitemap when there is more than one page, and
sweeps deleted pages off Sia in the same pass instead of leaving them
serving. A site saved before this opens as a single home page and gains the
rest when asked.
Two things that would otherwise be quietly wrong: assets exported from a
sub-folder page now carry the ../ that makes them resolve, and loading a
page into the editor no longer registers as an edit.
Sixteen entries in a <select> is a list of words: nobody can tell a blog
from a changelog without loading each one and undoing it. The gallery shows
what each template actually looks like and lets the categories narrow the
field.
The thumbnails are the templates themselves in sandboxed iframes, scaled to
the card. There is no screenshot pipeline to regenerate and nothing to go
stale — a template that breaks shows itself broken on its own card, which is
the point.
Two details the implementation has to get right. The previews are built
while the sheet is still hidden, so lazy loading would mark all sixteen
off-screen and never load any of them. And filtering with display:none tears
down each preview's browsing context, so every tab switch would flash white
while sixteen documents reload; hidden cards go out of flow at zero size
instead.
The first-run picker draws from the same map, so a new template appears in
both places and in the plain list behind them at once. That list stays for
the narrow bar layout.
The nine existing blocks covered a hero and the things under it. Anything
with a header, an FAQ, a team, a set of numbers or a signup band had to be
written by hand in the code widget — which is where people stopped.
Each new block keeps to its own class namespace, so stacking two of them
cannot make the first change shape: nothing here redefines a selector
another block already owns.
Two editor quirks are worked around in the markup rather than left for the
owner to trip over. Form buttons and table cells parse as opaque boxes
whose text is only reachable through the trait panel, so they carry
data-gjs-type=text and behave like every other word on the page (the
contact block's Send button gets the same treatment — it had the bug
first). And a bare <video> is given controls and a play bar by the editor,
which is wrong for a background film, so the video hero turns them off in
the model instead. Neither attribute survives export.
Panel icons now distinguish navigation blocks from page sections.
The four templates covered a launch page and a link hub. Everyone else —
a writer, a restaurant, a shop, a project that ships releases — started
from Blank and rebuilt the same scaffolding by hand. These twelve cover
the shapes people actually asked for, each one written to be overwritten:
plain copy that reads as an instruction, no images to source, no
JavaScript to keep working.
Every entry now carries its own card text and filter category, and the
top-bar list is generated from the map, so adding the thirteenth is one
object literal and nothing else.
Two choices do not need a select: a segmented control shows both options at
once, the active one is obvious, and the explanation moves to a short hint
beside it. Arrow keys switch it like a radio group.
The IntersectionObserver-based spy only fired when a section entered a
narrow band 40–45% down the viewport. Short sections at the tail of a
page — Operator key and Roadmap on theseus.x/extensions, whatever came
last on the other pages — never made it into that band because the
page runs out of scroll before their top crosses 40%, so their sidebar
links stayed dim no matter how far the reader scrolled.
Rewrote the spy across every page with a sidebar (theseus.x home /
plug-ins / extensions and sirius.x home / tld / theseus / docs / admin
/ brand): it listens to window scroll, picks the last section whose
top has crossed 35% of the viewport, and — when the window is scrolled
to within 4px of the bottom — snaps to the final section regardless of
where its top is. Every link in the rail can now activate.
theseus.x/extensions also had the spy IIFE bundled with the manifest
loader in one <script> block near the middle of the page, so at run
time the parser hadn't yet reached the last three sections and only
Bundled/Community/Build were ever registered. Moved the spy to its own
<script> just before </body> so it sees every section that ships.
The s3 field asked people for a Sia bucket key. Under Content & hosting it
is now a choice: not hosted on Sia, Sirius.X hosting (free, the name's own
folder on Sia, filled by Studio or the upload API), or another folder for
the few who host elsewhere. The raw key stays editable under the advanced
option; the on-chain record is unchanged.
The name summary listed two gateways; the public relay is navigate.st,
silentmode.st is the operator host. The summary, the Open and View
buttons now all point at navigate.st, and the s3 field explains what
Studio writes there.
Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.
Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
Star emoji swapped for the site's own 4-point sparkle mark — same SVG
that ships as the favicon — everywhere it appeared: the topnav brand,
the hero mark, the "you're here" pill on the family-of-.x panel, the
footer, register.html, portal.html, tld.html, market/, docs/, admin/,
brand/, storage/, theseus/, studio.html, site-footer.js. Anyone who
opens Sirius.X now sees the acid-green sparkle instead of a browser
star emoji.
Internal links normalised to no trailing slash — href="./theseus/"
became href="./theseus" (Theseus, docs, market, admin). Cleaner URLs
in the address bar and no double-slash redirect step on the mirrors.
Once stylesheets were inlined, the editor's default CSS parser read
them through the browser CSSOM, where a shorthand that contains var()
(background: var(--surface-1), border: 1px solid var(--border)) comes
back as empty longhands. Every card, panel and border on an imported
page vanished, leaving text on a flat dark background. The official
PostCSS parser plugin keeps those declarations intact; vendored like
the rest, no CDN.
Pages such as potidaea.asm put their background on <html> and leave
<body> transparent; the editor's base stylesheet paints <body> white,
so the imported page showed light text on white. The canvas body now
stays transparent, which also matches the exported page.
The gateway's public name route sends Access-Control-Allow-Origin so
Studio on sirius.x can fetch a page's stylesheets and inline them
instead of falling back to links; the route only serves public content.
The language picker had no toolbar to attach to in Studio and floated
over the blocks panel; pages can now mark a slot for it and Studio does.
An Assistant drawer in Sirius Studio with two providers and no key, no
server and no spend. "This browser" runs an open model on the user's
GPU through WebGPU with WebLLM (vendored, Apache-2.0); weights download
once from the MLC mirror and stay in the browser cache. "Local
endpoint" talks to an OpenAI-compatible runtime on the user's machine
(Ollama, LM Studio), which unlocks larger models on a real GPU. Nothing
the user writes or builds leaves their device in either mode.
Three verbs: make a section, rewrite the selected text, restyle the
selection. The model returns HTML and CSS as data; Studio sanitises it
(no scripts, frames, handlers or imports) and inserts it through the
editor, with Undo. Model output is never executed.
The quantisation is chosen per GPU: q4f16 when the adapter exposes
shader-f16, q4f32 otherwise (Pascal-era cards lack it). Small models
often answer with bare HTML instead of JSON, so the parser accepts
both, prompts avoid literal placeholders one model echoed back, and an
out-of-memory or disposed runtime is reported as "pick a smaller
model" with the engine reset. Switching models starts a fresh worker.
Verified on an NVIDIA Pascal card: SmolLM2 360M rewrites text, Qwen2.5
Coder 0.5B builds a section; the 1.5B f32 build exceeded that card's
memory and now fails gracefully.
The editor panels were a muddy grey with pinkish text (4.7:1) because
the webpage preset injects its own demo palette after ours. That theme
is now switched off and the editor is themed through GrapesJS's CSS
variables: dark panels, near-white text (15:1 and better), acid accents,
readable inputs and toolbars.
Editing is closer to what people expect from Tilda: every box has drag
handles for width and height, the text toolbar gains size, colour,
highlight, alignment and heading controls, the font list has the site
families, and selecting anything opens the style panel. Nine ready-made
sections (hero, features, image+text, gallery, testimonial, pricing,
call to action, contact form, footer) drop in with their own styles.
Plugins vendored from npm (no CDN): forms, navbar, tabs, tooltip,
custom code, countdown, background styles, touch support; GrapesJS
itself is already current at 0.23.6. The basic blocks (columns, text,
image, video, map) had never loaded: the bundle registers itself as
gjs-blocks-basic, not grapesjs-blocks-basic. VERSIONS.txt records what
is vendored and how to update it.
The editor's CSS parser drops :root, html and body rules, so an
imported site lost its variables, page background and fonts and came
up white. Those rules are now split out and carried verbatim: injected
into the canvas (as `html body` so they outrank the editor's base
sheet), stored with the draft, and emitted ahead of the editor's CSS on
export. The source page's <html> attributes (lang, data-theme, class)
travel the same way, since themed sites key their variables on them.
Templates reset all of it.
Sirius Studio only looked in its own folder, bns/<name>/. Every site
published the conventional way lives at bns/<label>/ (the s3 record),
or inline in the h record, so the editor opened those names as "new
site" with a template picker instead of the current build.
The gateway's site read gains a read-only ?src=live view that follows
the on-chain record: list and fetch from whatever bucket s3 points at,
or serve the h HTML as index.html. Writes are unchanged and still
confined to bns/<name>/.
The editor now starts from the live page when there is no draft:
relative image and link URLs are resolved against the public site URL,
stylesheets are inlined (cross-origin ones such as font CDNs are kept
as links and re-emitted on export), scripts dropped. When a draft
exists but the name serves something else, or index.html on Sia is
newer than the draft, a notice says so with a one-click import. An
"Import live site" button is always available. Verified with
aloevera.test, which is hosted at bns/aloevera/.
Reviewed both pages at desktop and phone widths. Desktop was fine; on
phones three things were wrong. The top bar wrapped into four rows
because the font-comparison pill and full-size links all stayed; on
screens under 640px the pill is hidden and links and buttons tighten.
The dashboard menu rendered as five full-width blocks because the
wide-screen width:100% rule outranked the phone override; it is now a
row of pills, the wallet block collapses to one line (short address,
live balance, refresh), and the sub-tabs scroll sideways instead of
wrapping. The long address had also been pushing the whole page wider
than the viewport, fixed with minmax(0,1fr) on the grid column.
The market showed an empty filter bar and a one-line status when there
were no listings; it now shows a proper empty state with the two things
a visitor can do (sell from the dashboard, register a new name) and
hides the filters until there is something to filter. Buy buttons go
full width on phones.
Ariadne's resolver copy and the Android indexer still admitted every
REG under a cosign or hidden TLD, so they disagreed with the gateway
and Theseus about which names exist. Ariadne now carries the current
Argus resolver-web.js verbatim (the copy had drifted: no TLD beacon, no
owner tracking, old electrum list); its daemon only imports buildIndex
and normalizeName, both unchanged. Bns.java gains the same rule in
Java: walk the TLD beacon, keep each TLD's TUPD policy timeline, and
drop a REG whose TLD required a co-signature at that height unless the
transaction re-issues the TLD certificate. If the TLD beacon cannot be
read the mobile index degrades open rather than empty. Needs an APK
build (0.19) and an Ariadne zip rebuild to reach devices.
Dashboard balance: it was read once from the gateway at sign-in and
never again, so payments and spends never showed. The sidebar and
Settings figures now come straight from electrum every 15 seconds
while the tab is visible, after every broadcast the page makes, on tab
focus and on a new refresh button; unconfirmed coins count as spendable
(listunspent includes the mempool) with a small "incl. N sat
unconfirmed" line. The header wallet pill asks the dashboard for a
fresh figure when opened, and the register flow keeps its stored
figure current while it watches for funding.
The dashboard was capped at 1200px with single-column lists, so a
1920px display showed a narrow strip of cards. Above 1280px it now
widens to 1720px with a 260px sidebar, name/TLD lists become card
grids (auto-fill, 360-400px columns), and Settings and TLD detail lay
their cards out in two or three columns. Narrow screens are unchanged.
The header Dashboard button was hidden until sign-in, which left new
visitors with no obvious way in; it is now always shown and lands on
the portal's sign-in when signed out.
Also closes a Settings-pane markup bug: the Wallet card ended one div
early, which pushed the "Reading, language & help" and "This device"
cards outside the pane so they appeared under every other pane.
Two gaps the owner panel left open. First, a hidden TLD was only a UI
gate: anyone could still broadcast a REG under it and every indexer
admitted it. Second, there was no way to sell a name without trusting
the other side.
Co-sign rule (consensus, applied in lockstep by bns.js and
resolver-web.js): a REG under a TLD whose records at that height say
policy "cosign" or hidden 1 is indexed only if the transaction carries
the TLD's own certificate. The certificate can only be spent by the
owner's key and is re-issued to them in the same transaction, so it is
a co-signature nobody can forge and nothing is consumed. The TLD map
now keeps the TUPD timeline so policy is evaluated at the REG height.
Owners register under their private TLDs with the certificate added
from their own wallet; third parties under a "cosign" TLD build the
full transaction, sign their inputs and queue it at /api/cosign, where
the owner approves it from the dashboard (signCosignRequest refuses to
sign unless the certificate returns to the same locking script).
Marketplace: a listing is the seller's certificate input plus a price
output signed SIGHASH_SINGLE|ANYONECANPAY, stored by the gateway as a
bulletin board (/api/market, verified against the on-chain owner and
pruned when the certificate moves). The buyer completes it in one
transaction, so the seller is paid exactly when the name moves.
Cancelling also spends the certificate once so the offer is void.
Site: market.html, Sell sub-tab and Pending approvals in the portal,
Market link in nav and footer, six dictionaries extended, cache tags
bumped. Verified on chipnet: cosigned.sc registered by a throwaway
wallet through the queue with the .sc certificate back at the owner;
aloevera.test listed and delisted through the API.
Ariadne's resolver-web.js copy and the mobile Bns.java port still need
the co-sign rule; until then they admit REGs this index rejects.
The wallet dropdown had become a navigation menu (My names, Admin panel)
while wallet facts were nowhere. Now a Dashboard button sits left of the
wallet pill on every page while signed in, and the wallet menu shows the
address, balance and holdings, copy address / token address, the faucet,
wallet settings and sign out. The admin panel moves into the dashboard
sidebar and only appears for the operator wallet. Translations included.
Studio chrome, template picker, publish dialog, the shorter dashboard
menu and the Settings copy in all six languages. Dictionary cache version
bumped.
BuiltInWallet.signMessage read `recovery` from libauth's recoverable
signature, but libauth v3 names it `recoveryId`. The flag byte came out as
0, so verifiers recovered the right key only when the true recovery id
happened to be 1. Signed DNS manifests and Studio uploads failed with
"signature does not match current on-chain NFT owner" and a different
derived address each attempt. Read the right field, fail loudly if it is
missing, rebuild the browser bundle and move every importer to the new
bundle URL. Also lands the registrar's signRecordsManifest that the bundle
already shipped.
Owners had no way to put a real page behind a name without running Sia
tooling themselves. Sirius Studio (studio.html, js/studio.js) embeds
GrapesJS (BSD-3, vendored under vendor/grapesjs so nothing loads from a
third party) with three starter templates; Publish exports one
self-contained index.html plus the editor project, uploads both to the
name's own folder bns/<name>/ on Sia, and sets the s3 record on chain if
the name does not point there yet. Drafts save to the same folder.
Gateway: /api/site/<name>[/<path>] — list, read-through, PUT and DELETE.
Every write carries a wallet signature over (name, path, body hash,
timestamp) that must recover to the current NFT owner, and writes are
confined to bns/<name>/ so no name can touch another name's bucket.
Dashboard menu is now Web-Builder, Domain names, My TLDs, Settings, Sign
out; Overview and Wallet folded into Domain names and Settings, Docs is a
link in Settings.
Menu, panes, DNS table, transfer, export, settings and the new session
copy get German, Greek, Russian, Spanish, French and Portuguese entries;
Greek and Russian keep the "blockchain" wording. Dictionary cache version
bumped so returning visitors pick the new strings up.
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.
Same reasoning as the Russian fix: "αλυσίδα" is a literal "chain" that
Greek readers do not use for a blockchain, which is normally written as
the loanword. Every Greek string now uses "blockchain" with the neuter
article in the right case. Dictionary cache version bumped.
"Цепочка" is a literal rendering of "chain" that Russian readers do not
use for a blockchain. The headline now states plainly that these are
domain names on the blockchain, and every other Russian string uses
"блокчейн" in the right case. Dictionary cache version bumped so
returning visitors pick up the new wording.
The native select sat mid-nav next to the font pill and was easy to miss.
Replace it with a custom dropdown placed just before the wallet button:
globe icon, the current language's flag and native name, a menu of all
languages with flags. Flags are tiny inline SVGs rather than emoji
(Windows renders flag emoji as bare letters) or a flag CDN (no third-party
request from the visitor's browser). Painted on window load so it lands
after the deferred nav scripts; keyboard: arrows, Escape, click outside.
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
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.
Design translated from the /design canvas review. Sirius.X now shares
Deviant's "signal" theme background (layered sky-rim/mid gradients over
#050810) and the Silent Mode family typography stack.
Typography:
Fraunces (opsz variable serif) headings h1/h2/h3
DM Sans body / UI
JetBrains Mono code, hashes, mono cells
Ubuntu opt-in Bitcoin Cash brand font,
swaps everything at :root when
<html data-font-cmp="ubuntu">
All via Google Fonts, one <link> per page.
Font picker (js/theme.js):
Small pill in the topnav (Fraunces default / Ubuntu alt) — persisted
to localStorage.sirius-font. Sets the data-font-cmp attribute; the
swap CSS lives in each page's <style> block so !important overrides
page-local font-family declarations at the necessary specificity.
Mono elements stay JetBrains Mono under both fonts.
Palette (cool sky vs old flat dark navy):
--bg #0b0e14 → #050810
--panel2 #18202c → #0a0f1c (matches Deviant --sky-mid)
--line rgba(255,255,255,.09) → .06
--ink #e7eaf1 → #f1f4fa
--mut #8b98a9 → #b8c2d4
--dim #5e6678 → #6a7488
new: --sky-rim #131a2a, --chain-native #0AC18E (BCH brand green,
reserved for chain-native badges/proofs like the Bitcoin.Cash
body copy)
Copy — "no renewals" softened everywhere it appeared. Renewals will
be a per-TLD policy set by each TLD owner; the flat claim was wrong
across the board. Landing card, tld.html hero and body, plus the
og:description all updated.
Applied to: index.html, portal.html, tld.html, theseus/index.html,
brand/index.html, docs/index.html, admin/index.html.
TLD price tiers (pricing.js):
1 char $500 (was $10)
2 char $250 (was $8)
3 char $150 (was $6)
4-6ch $100 (was $5)
7-10ch $50 (was $5)
11+ch $25 (was $4)
Payment approval (register-flow.js):
New stepApprove(...) gates the actual sign+broadcast on an explicit
user gesture — PIN pad when a PIN blob exists on this device, wallet
password otherwise. Same "3 wrong PINs → wipe, fall back to password"
behavior as the sign-in unlock step. Wired between stepConfirm(Tld)?
and stepRegister(Tld)? so both name mints and TLD mints require
approval even when the wallet is already in memory.
tld.html fee card: reflects the new $25–$500 range instead of the
"≈ 1,250,000 sat · ≈ $5" placeholder.
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.
Two issues reported after an Import sign-in:
1. Done button was stuck — user saw the ✓ Signed in screen but the
portal never switched to the names view. Root cause: finishSignIn
fired siriusProfileChanged BEFORE setting window.siriusWallet, so
the portal's listener called adoptWalletFromModal() while
window.siriusWallet was still null, saw no wallet, and did nothing.
Fix: expose the live wallet BEFORE writeProfile so the sync
listener sees it and can enterPortal() immediately.
2. Every reload asked for the password again. Cache the wallet's
mnemonic in sessionStorage on sign-in — same tab (or a page
reload) rebuilds the BuiltInWallet silently via
BuiltInWallet.fromMnemonic; a full browser close clears
sessionStorage and the user is back at the Unlock tab.
sessionStorage is per-origin per-tab so an XSS on Sirius.X pages
would still be able to read it — that's the tradeoff for the
convenience. Chipnet only; mainnet gets the PIN escrow pattern
(3 wrong PIN tries → escalate to password) that Digibyte.x/web
already uses. PIN implementation is deferred to its own commit.
portal.html adoptWalletFromModal now tries sessionStorage after the
in-memory check; register-flow.js startFlow/startTldFlow do the same
via a new async adoptSessionWalletAsync so name and TLD mints on any
page reuse the session wallet with no re-prompt. profile-menu.js
sign-out clears sessionStorage + window.siriusWallet so signing out
really does drop the user.
Single source of truth (js/pricing.js) mirrored on landing, tld.html
and the mint flow — same label always shows the same price everywhere.
Name tiers (label part):
1 char $5.00 premium-1
2 chars $3.00 short-2
3 chars $2.00 short-3
4–5 chars $1.00 standard
6–7 chars $0.50 long
8–16 chars $0.25 extended
17+ chars $0.10 very-long
all-digits ×0.5 (less brandable)
contains hyphen ×0.7
TLD tiers (label part):
1 char $10.00 premium-1
2 chars $8.00 short-2
3 chars $6.00 short-3
4–8 chars $5.00 standard
9+ chars $4.00 long
Rendering:
- Landing #search-results: acid-tinted price badge on each available
row; grid grew a fourth column so name + badge + price + button
all sit on one line
- tld.html search result-card: same price badge next to the Register
button on available TLDs
- tld.html registered-TLDs table: new 'Mint price' column showing the
tier price for every registered TLD, so buyers see the pricing
ladder next to concrete examples
Mint pipe:
- startFlow(name)/startTldFlow(label) resolve the label price via
window.siriusPricing (default) but honour opts.serviceFeeSats when
the caller passes one — leaves the door open for coupons /
operator discounts / oracle overrides in a future revision without
reshuffling call sites
- The overridden service fee is threaded through quoteRegistration
AND registerWithBuiltInWallet/registerWithExternalWallet so the
confirm screen and the on-chain output agree
Chipnet placeholder rate (250,000 sat/USD) stays in pricing.js;
mainnet will swap in a live BCH/USD oracle. Operator-side discounts
and coupon codes are the next iteration — deliberately not disclosed
in this shipping copy.
Unlock tab used to be hidden when no saved wallet existed on the
device — so a fresh browser saw a 3-tab strip, and a returning
browser saw a 4-tab strip. Inconsistent, and users kept asking where
Unlock went.
Now Unlock is always the first tab:
- if a saved wallet exists, stepUnlock() renders the usual password
form (nothing else changes)
- if no saved wallet, stepUnlock() renders 'No wallet on this device'
with a one-line explanation and two shortcut buttons — 📥 Import
and 🆕 Create — that jump straight to those tabs
Same tab bar renders in all cases so the layout is stable across
sessions and between devices.
Nav wallet dropdown was calling window.siriusSignInWallet(action),
which opens the shared modal on whatever page you were on — so from
tld.html or docs/, a Create/Import/WC click drew the sign-in over the
current page as a full-screen overlay. User: 'still opens as a
separate console on top of the page, it should be within the page'.
Now the click handler:
- if we are already on portal.html AND
siriusRenderSignInInline is loaded, paint the tabbed sign-in
inside portal's #signin-inline directly and scroll to it
- otherwise navigate to portal.html?mode=<action>, where the
bootstrap already renders the tabbed sign-in inline as the
page's own content
Same tabs, same forms — the four options (🔓 Unlock / 🆕 Create /
📥 Import / 🔗 WizardConnect) — but they now live inside the page,
not on top of it. Unlock falls back to Import when there's no saved
wallet in that browser.
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.
renderSignInInline() reassigned the module-level 'sheet' variable to the
host page's container, but render() had been shadowing that with a
separate 'mountTarget' constant captured at module init — so every step
kept writing into the modal sheet instead of the inline container.
Portal.html therefore showed an empty #signin-inline while the tabs +
form rendered off-screen inside the (closed) modal.
Drop mountTarget entirely and let render() write directly into 'sheet'.
'sheet' is already the reassignable target for both modal and inline
paths, and renderSignInInline / close() already keep it in sync.
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).
/sirius-x/portal.html?mode=import was landing users on the old three-card
inline sign-in UI (New / Import / WizardConnect each as its own form).
The new UX is: one tabbed modal, everywhere. Portal was the one page
still shipping the legacy cards.
Replaces the whole <section id='signin'> with a single launcher card
('🔑 Sign in or create wallet' → opens the shared modal). The old
handlers (, , etc.) are
comment-blocked out; new script uses window.siriusSignInWallet().
?mode=new|import|wc|unlock auto-opens the modal at that tab (the same
handler dropdown items use). If the user arrives with a siriusProfile
but no in-memory wallet (e.g., they signed in on another page), portal
opens the modal at Unlock (or Import when no saved wallet). Sign-out
delegates to the shared signOutUi(). Cross-tab and same-tab profile
changes both re-adopt.
register-flow.js: finishSignIn now sets window.siriusWallet so portal
(and any other page loaded in the same tab) can pick the wallet up
without re-prompting for a password. In-memory only — reload/nav clears
it, and the profile listener falls through to Unlock.
Two features:
1. Operator can hide TLDs from the public /api/tlds listing so hidden
TLDs stop appearing in name-search UIs. On-chain registrations
under a hidden TLD keep resolving — this is a UX filter, not
enforcement.
Gateway (public-gateway.mjs):
- Persistent HIDDEN_TLDS set backed by hidden-tlds.json next to
the service script
- GET /api/tld-visibility -> {hidden:[...]} (public)
- POST /api/tld-visibility -> updates the list (operator-gated by
Bearer BNS_OPERATOR_TOKEN env var; if unset, all writes refused
so we default-deny)
- /api/tlds filters out HIDDEN_TLDS; add ?include_hidden=1 to see
everything (used by the admin panel to show all rows)
- Operator token installed via systemd override on the VPS
Admin panel:
- New 'Operator token' card at the top of the TLD-registry section;
token stored in sessionStorage (not localStorage) so a full
browser close forgets it
- Each TLD row got a 'Hidden from public' checkbox that POSTs on
toggle and refreshes the table; failures roll back the checkbox
and surface the error next to the token field
2. Wallet dropdown restored to 4 direct actions
(Unlock / Create a wallet / Import a wallet / WizardConnect) and
the shared mint/sign-in modal grew a tab strip so users can switch
between the four wallet actions from any step without going back
to a choice screen.
register-flow.js:
- renderTabs(active) prepended to stepCreate/stepImport/stepUnlock/
stepExternal when signInOnly is set. Unlock tab only appears
when a saved wallet exists.
- Delegated click handler on the sheet routes tab clicks to the
matching step; switching away from a live WC session tears it
down first so we don't leak WebSockets.
profile-menu.js:
- Restored 4-item onboarding menu (Create/Import/WC plus Unlock
when saved). Each item is a direct entry point; the tabbed modal
lets the user pivot to any other option without closing.
Cache-buster bumped on all 8 sirius-x pages to ?v=20260908tabs.
Two changes:
1. Wallet dropdown simplified from three direct-action items into one
'Sign in or create wallet' launcher (plus 🔓 Unlock when a saved
wallet is present). The single item opens the shared modal at
stepWallet — the choice screen showing all four options (Unlock /
Create / Import / WizardConnect) as clear cards. Clicking a card
reveals its form. Users see the options first, then commit to a
path, instead of landing on a form for one path without seeing the
others.
register-flow.js's stepWallet grew a signInOnly-aware header ('Sign
in or create a wallet' / 'Pick one. Your keys stay in this browser')
so the choice screen reads as sign-in context, not a name mint.
startSignIn still accepts direct-mode entry points (new/import/wc/
unlock) for deep-links like portal.html?mode=X, but the nav dropdown
funnels to 'choose' for the choice-first experience.
2. Theseus subpage copy updated per request: 'native .bch / BCNR name
resolution built in' -> 'native Bitcoin Cash Domain Names resolution
built in'. Meta description mirrored.
Previously, the only way to unlock a wallet that had been saved in this
browser was to click Register (name or TLD), open the modal, and pick
'Unlock my browser wallet' from the wallet-choice step. From the nav
dropdown you could only start onboarding fresh (New / Import / WC).
profile-menu.js now checks localStorage for the BuiltInWallet's storage
key (bns.wallet.v1) at every render, and when a saved wallet is present
inserts a '🔓 Unlock my wallet' item at the top of the not-signed-in
dropdown — with a divider below it, so it reads as the primary action
and the Onboarding options stay available for adding a different wallet.
The dropdown click handler already lazy-loads register-flow.js and
calls window.siriusSignInWallet(action); register-flow.js's startSignIn
grew an 'unlock' mode that opens the modal directly at stepUnlock (the
password prompt). Same success path as every other sign-in: on unlock
success, siriusProfile is written and the modal closes with '✓ Signed
in' — the nav pill flips to the address without a reload.
Verified end-to-end on landing: saved wallet detected -> dropdown shows
Unlock as first item -> click -> password -> '✓ Signed in' -> pill
becomes 'qqyx49…zx8x seed ▾' with no reload. TLD mint from tld.html
also verified end-to-end: search 'e2etldtest' -> Register -> modal ->
Unlock -> password -> Fund -> Confirm (fee 1,250,000 sat + beacon dust
~1,300 sat) -> Register -> checking-availability -> loading-coins ->
'wallet is empty' (expected without chipnet funds; downstream code is
the same registerTldWithBuiltInWallet path the CLI uses).
Two UX polishes:
1. Register-tab icon flipped from 🪪 (identification-card emoji, renders
as a hollow box in system-ui fonts without extended emoji) to ✏️
(pencil), which reads as 'edit/create' and ships in every emoji font
worth targeting. Changed across the nav on all 8 pages plus the
footer injector's Product column. Kept the Register nav item — six
items total in the nav, and from any subpage it's one click to the
search on landing.
2. tld.html no longer duplicates the wallet-choice + mint flow inline.
The Register button on a search result now hands the label off to
the shared register-flow.js modal — same modal that name registration
uses — via a new window.siriusRegisterTld(label, {serviceFeeSats})
entry point.
register-flow.js grew:
- startTldFlow / window.siriusRegisterTld: sets state.tld and walks
the modal through wallet-choice → Fund → stepConfirmTld → stepRegisterTld
→ stepDoneTld
- stepConfirmTld: shows label + fee + owner, no per-name quote
- stepRegisterTld: dispatches to registerTldWithBuiltInWallet or
registerTldWithExternalWallet based on state.wallet/state.session
- stepDoneTld: 'X is yours' with txid + certificate id + operator
address, no record editor (a TLD certificate has no records to
set immediately)
- stepWallet / stepExternalConfirm branch on state.tld so the modal
heading and the WC-confirm path route correctly
tld.html trimmed: removed the entire signin-section + mint-section
plus their inline handlers (~250 lines gone). Now just search +
registered-TLDs list + a delegator to the shared modal. If a user
is already signed in via the wallet dropdown, the modal recognises
the saved wallet ('Unlock my browser wallet' button appears) instead
of asking them to sign in again on this page.
Wallet dropdown items (New / Import / WizardConnect) were navigating
to portal.html?mode=X and asking the user to click again on arrival.
That is 'the sign in landing page which is not functioning' from the
user's perspective — a whole redirect for one form.
Now every dropdown item opens the mint modal (register-flow.js) inline
at the matching step, on whatever page the user is on:
New -> stepCreate ('Create your wallet' — password + generate)
Import -> stepImport ('Import a recovery phrase' — textarea)
WizardConnect -> stepExternal ('Open your wallet' — QR/URI)
register-flow.js grew a signInOnly mode: state.name is null, the step
Back buttons close instead of going to a wallet-choice step there is
no context for, and on wallet-loaded the flow writes siriusProfile
and shows a 'Signed in' confirmation instead of Fund -> Confirm -> Mint.
WC signInOnly keeps the session alive (state.session) so a later
record edit can reuse it without a fresh QR handshake.
profile-menu.js: menu items became <a data-action='new|import|wc'>
and the click handler lazy-loads register-flow.js on demand — the
docs/brand/theseus pages don't ship it in their initial payload, so
their nav pill loads it the first time a wallet button is clicked and
caches it for subsequent opens. Falls back to portal.html?mode=X if
the module can't load. Cache-buster bumped so cached copies pick up
the new behavior.
Importmap for @bitauth/libauth added to docs/, brand/, theseus/ so
the bundle's bare specifier resolves when register-flow.js is
lazy-loaded from those pages.
Verified end-to-end on the live docs page: all three dropdown items
open the modal inline at the correct step, no console errors, no
navigation.
Register.html and the landing were running duplicate name-search UIs.
The register one was reported broken; the landing one already works
inline. Merged both into the landing:
- New js/register-flow.js — the full mint modal + wallet setup (Create /
Import / Unlock / WizardConnect / Fund / Confirm / Register / Point
it somewhere) extracted from register.html's <script type='module'>
and turned into a shared module. Injects its own scoped modal HTML +
CSS into the host page on load and exposes
window.siriusRegisterName(fullName) as its public entry point.
- Landing search results now have Register buttons that call the shared
flow directly. The whole 'search -> pick name -> mint' journey stays
on one page, no redirects, no duplicate search UI to maintain.
- register.html reduced to a redirect stub: preserves ?q= if present,
refreshes/JS-forwards to './' (landing), and shows a one-line
'Name search moved to the home page' fallback for JS-off users.
Old links keep working; no /register.html deep links are broken.
- All nav 'Register' entries now point at './#search-input' (or
'../#search-input' on subpages), scrolling straight to the landing
search box. Footer injector and every internal href updated the
same way. Zero live href='.../register.html' left in the tree.
Verified: register.html?q=hello redirects to /?q=hello; landing
search 'fresh42abc' -> Register button -> modal opens with
'Register fresh42abc.bch' at the wallet-choice step.
Three UX fixes:
1. Beautiful footer on every page. Extracted the 4-column landing footer
into js/site-footer.js — reads the nav brand link to derive per-page
base ('./' at root, '../' in subdirs) so links resolve from any depth
without duplicating the HTML across 8 pages. Every page now ships an
empty <footer id="site-footer"></footer> and includes the injector;
inline footer + its CSS block removed from landing too so the source
stays in one place.
2. Wallet button redesign. Pill now shows a state dot (dim when not
signed in, green with subtle glow when signed in), a monospace short
address label when signed in ('qra6rs…7yl2') OR 'Wallet' when not,
a WC/seed badge on signed-in state, and a subtle caret. Full border
and hover states, proper aria-expanded/aria-label wiring. Dropdown
menu itself upgraded: grid layout per item (icon column + title +
hint), heavier backdrop blur, larger min-width, clearer typography.
3. Dropdown options actually do something. Removed the redundant 'Open
sign-in page →' item — the other three all land on portal too, so
listing 'open the page' as a fourth option was noise. On portal
arrival, the ?mode= handler now AUTO-TRIGGERS the primary action for
each mode:
new -> click Generate a phrase (12 words appear immediately)
import -> focus the seed textarea (cursor ready to paste)
wc -> click Connect wallet (WC session starts, URI shown)
No extra clicks between dropdown choice and the flow it names.
Cache-buster on the profile-menu.js include bumped to
?v=20260907wallet so cached copies pick up the new design; footer
injector at ?v=20260907rel.