Pairing already worked; signing would have thrown on the first request
a dapp ever sent. Found by testing against the real relay and the real
@wizardconnect/wallet library rather than reading the code.
Two bugs in wc-sign.js, both fatal:
- The WC message nests the whole WcSignTransactionRequest under
`.transaction`, so the tx is at request.transaction.transaction and
the spent outputs at request.transaction.sourceOutputs. We read
request.transaction as the tx and request.sourceOutputs as the
outputs, so tx.inputs was undefined. index.js already read the nested
request.transaction.userPrompt for the approval dialog, so only the
signer had it wrong. The flat shape is still accepted.
- generateSigningSerializationBCH takes TWO positional arguments,
(compilationContext, {coveredBytecode, signingSerializationType}).
We passed one merged object, leaving coveredBytecode undefined and
throwing inside libauth. For P2PKH the covered bytecode is the spent
output's locking script.
Now verified end to end: a two-input transaction spending from two
different derivation paths signs, decodes, and passes
createVirtualMachineBCH().verify() — consensus-valid, with
SIGHASH_ALL|FORKID|UTXOS (0x61) on every input as the protocol
requires.
Also: RelayStatus is an object ({status: "connected" | "reconnecting" |
"disconnected" | "session_deleted"}), and the snapshot read a
non-existent `.kind`, so every connection reported the literal
"[object Object]". Reads `.status` now, uses the documented
getConnections() accessor instead of the private connections Map, and
carries the library's own `label` ("dapp name once known, otherwise
Connecting…"). The panel shows a tag for anything other than connected
— "reconnecting" is the difference between a pairing that will see the
next signature and one that is dead, which was invisible before.
0.9.3 merged ✎ and 🗑 into one ⋯ in the coin drilldown but left the
coins summary row with the old pair, so the two views disagreed about
the same idea. Both now carry a single ⋯ opening the manage modal.
Found by rendering the panel against stubbed host state over HTTP
rather than reading the diff — the file:// preview never executes
panel.js, so earlier checks could only confirm the markup existed, not
that it drew correctly.
Drops the now-unreachable removeWalletWithConfirm helper, the
data-wremove handler and the .wactdel style that went with them.
Address row is now icon · label · amount · one button. ✎ and 🗑 were two
controls for what is really one idea ("change this wallet"), and the
manage modal already holds rename, derivation path AND remove — so a
single ⋯ opens it. That also stops Remove sitting one stray click away
from Rename. Default/legacy wallets show a lock instead.
The per-address amount is back unconditionally: 0.9.2 hid it when a coin
had one address, but the row reads as a breakdown and the gap looked
like missing data. The duplicate that actually mattered — the drilldown
subtitle — stays gone.
The Assets card only ever had branches for SOL, BCH and TRX, so ETH fell
through to a hidden card despite the adapter returning tokens in the
same shape. The generic branch is now keyed on the DATA rather than a
list of chain ids: any chain whose snapshot carries `tokens` renders,
which means ETH works today and a chain added later works for free. Only
the label ("TRC20" / "ERC20" / "Tokens") varies by chain.
Dropped the "Pick a coin" title row from the coin picker — the search
field's own placeholder already says what the pane is, so the title was
a line of chrome restating it and pushing the list down. Search and
close now share the top row.
Token history stays in History despite the request coming from tokens
being mistaken for transactions: a wallet that had only ever moved USDT
still showed an empty history without it.
Receive was ordered QR → address → tokens, so 200px of always-on QR sat
above the thing people came for and pushed the asset list off screen.
- Address first, with Copy and a QR button; the QR expands inline and
the choice sticks, because someone who receives by QR wants it every
time and someone who copies never does.
- Explorer and Faucet moved up to the header status row. They act on the
selected wallet, not on the act of receiving, and down there they
competed with Copy for the one row that gets used.
- Assets card renamed from Tokens and now leads with the native coin, so
"what does this wallet hold" is one list rather than two places.
- "+ Add another <TICKER>" moved below the address list.
The balance appeared three times — header, drilldown subtitle, address
row. Now once in the header; the subtitle keeps only the per-unit price,
and the per-address amount returns when a coin actually has more than one
address to compare. The address row drops its truncated address (the full
one is at the top of Receive) and keeps the wallet name.
The per-address asset list added in 0.8.8 duplicated the Assets card and
is removed — assets live in one place.
Token amounts were unreadable: an 18-decimal balance rendered as
60000000.000000005435817984. Capped to 8 decimals with thousands
separators, exact value on hover.
A symbol claimed by more than one contract now carries a LOOK-ALIKE tag.
The test wallet holds four different contracts all calling themselves
"Test USDT" — spam mints borrowing a trusted ticker so a careless send
lands on the wrong one. We can't tell which is genuine, so we mark every
member of the clash rather than guessing.
History showed native transfers only, so a wallet that had only ever
moved USDT looked empty. TRC20 transfers are merged in newest-first, each
carrying its own decimals and ticker (rendering a token against the
chain's scale would be off by orders of magnitude). Both feeds are on by
default; the checkboxes narrow rather than opt in, and the last one
checked can't be unchecked into an empty list.
Mounting an imported TRX/ETH/SOL wallet read the optional custom RPC as
String(stored || undefined), so a wallet with no override got the literal
"undefined" as its server: every history and token fetch went to
"undefined/v1/accounts/…" and the panel showed "undefined" under the
balance. Only a real https URL overrides the network default now, and the
adapter itself rejects anything that does not look like one.
Completes the three detection paths. The injected provider shipped in
0.8.8; these two needed host support, because nothing in the add-on API
could reach the active tab's content (captureTab is pixels, not DOM).
wiz:// links (main.js)
A click on a wiz:// anchor is intercepted in will-navigate and in the
window-open handler (target="_blank" lands there instead), and routed
to the wallet with the offering page's origin attached, so the
approval names the real site. The tab never navigates. This needs
nothing from the dapp beyond rendering the URI as a link, so it works
for third-party dapps that will never adopt a Silent Mode API.
scan-page capability (addons-host.js + main.js)
New capability backing api.scanActiveTabForUris({scheme, limit}).
Deliberately NOT a "read the page" API: the host runs the match and
returns only the URIs found, so an add-on holding this still cannot
see page text, markup or form values. It sits well below page-inject
on the trust ladder — it learns that a page offers a wiz:// code and
nothing else. Scheme is validated against [a-z][a-z0-9+.-]* and the
result count is capped.
The matcher also accepts WizardConnect's QR-alphanumeric spelling
(WIZ://%3FP%3D…), which is frequently the only form present when a
dapp renders its pairing code as a QR, and decodes it. Verified
against the SDK: decodeKeyExchangeURI accepts standard, QR-raw and
QR-decoded alike.
Regex sources are built host-side and passed as JSON rather than
assembled inside the injected string — hand-escaping backslashes and
quotes through two levels of literal was both wrong on the first
attempt and unreviewable.
Aegis
Declares scan-page, adds the wcScanPage handler and a "Scan page"
button next to Connect. A scan fills the URI field and stops there
rather than pairing outright: the user still chooses which wallet
signs and still presses Connect, because a scan that silently paired
would carry far more consequence than the button implies. Older hosts
without the capability get a clear "update Theseus" message instead of
a dead button.
Completes the parity work 0.8.7 started for Tron. Imported ETH and SOL
wallets showed a native balance and nothing else, because the JSON-RPC
endpoints they poll have no history or token concept at all.
- ETH history + ERC-20 balances via Blockscout, which needs no API key
(Etherscan V2 does). Mainnet RPC moves off eth.llamarpc.com, which was
answering 525 with an HTML error page — that parsed as a JSON error and
showed as a 0 balance.
- SOL history via getSignaturesForAddress and SPL balances via
getTokenAccountsByOwner, both keyless on the public RPC.
Three things the live testing turned up:
- A Blockscout mempool entry is {result:"pending", status:null}. Reading
that as "not ok, therefore failed" showed pending sends as failures.
Now carries a distinct pending state through to the row.
- History `delta` is now a number, a decimal string, or null. ETH wei
needs the string (18 decimals overflows a JS number, and Math.abs was
silently rounding it); Solana's signature feed carries no amount at
all, and null >= 0 is true, so unknown amounts were rendering as a
"+" that claimed a receive we cannot verify. Unknown now renders as a
neutral row instead.
- A real address came back with 855 ERC-20s and 3078 SPL mints, nearly
all airdrop spam, some with blank, zero-width or bidi-override
symbols that render as an empty row borrowing trust from its
neighbours. Token text is sanitised and lists are capped at 50, sorted
so named tokens survive the cap.
Also: the first-run setup screen forced text-align:left on the form, so
its helper copy ran ragged under a centred mark, title and description.
The form now inherits the centred alignment; the mnemonic box stays
left-aligned on purpose, since centring wrapped seed words makes them
harder to check.
Wallet strip:
- Clicking a coin opens that coin's page (addresses, price, totals, back
and close) instead of only flipping the selection and leaving the list
sitting there. The page already existed but was reachable only via the
small count chip.
- The per-coin second action was a gear that selected the wallet and
opened the global Settings tab — the same destination for every coin,
so it read as a per-coin control that wasn't one. It is now Remove,
behind a confirm, with the default/legacy wallet showing a lock
instead since it gates legacy funds.
- Each address in the drilldown can expand to show what THAT address
holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via
tokenBalances. walletSummary now carries both per wallet, so the view
no longer has to borrow the selected wallet's assets.
WizardConnect — the Connect pane was effectively unusable:
- The locked-vault branch told the user to unlock and gave them nothing
to click. It is reachable without the lock screen ever appearing,
because a mounted imported wallet makes overallPhase read "ready".
It now carries the same unlock form the lock screen uses.
- Imported BCH wallets were never registered with the WC manager —
startForWallet ran only in the vault-derived mount branch. They mount
as ready, so they appeared in the "Sign with" picker and then failed
on pair. They now register from their stored seed. WC derives a child
key tree, so single-key (WIF) imports genuinely cannot pair; those are
disabled in the picker with the reason, rather than failing on click.
- Adds window.wizardconnect so a dapp can hand over the wiz:// URI it
already generated instead of making the user copy it between tabs.
The protocol is Nostr-relay pairing designed for phone-scans-QR, and
the SDK has no in-page discovery at all, so this is our own surface:
connect() + isReady(), plus a wizardconnect:announceProvider event
shaped like EIP-6963 so several WC wallets can coexist. Pairing always
goes through the approval modal; the URI is validated before any UI
shows, and the wallet never reads the page to find one.
Imported Tron wallets pointed at api.nileex.io, which only serves the
/wallet/* JSON-RPC family and 404s all of /v1/. That REST family is
where transaction history and the trc20 balance map live, so an
imported Nile wallet showed a native balance and nothing else. The
built-in Tron adapter was already on nile.trongrid.io, which is why
only imports were affected.
Switches the imported Nile endpoint to nile.trongrid.io and fills in
the two features that were never implemented for imported account-
model wallets:
- History via /v1/accounts/<addr>/transactions, with the signed delta
computed by comparing owner_address against the wallet's own address
in 41-hex form (the feed returns hex regardless of visible:true).
- TRC20 balances via /v1/accounts/<addr>, joined against token_info
harvested from recent trc20 transfers to recover symbol + decimals.
Both are best-effort so a chain with no keyless feed can't blank a
wallet whose balance fetch succeeded. Contracts with no registry entry
render as "Unknown token" with a raw amount rather than a number
invented from assumed decimals, and named tokens sort above them so
airdrop spam can't bury real holdings.
window.confirm/alert render as chrome-owned, Theseus-branded OS boxes
outside the sidebar, which breaks the illusion that Aegis is one
coherent surface — and they can't carry an icon, a danger-styled
button, or formatted copy.
Adds aegisConfirm() / aegisAlert(): the same overlay shell the manage
and import modals already use, resolving like confirm() so callers
just await it. Escape cancels, Enter confirms, click-outside cancels.
Swapped at all four confirm sites (remove wallet from the picker,
remove wallet from Settings, remove PIN, sign out) and all seven
alert sites.
Every Coin-Spectrum poll silently returned an empty map because we
were reading body.price_usd (top-level) while the API wraps its data
under body.asset. Every chain's Number(undefined) came back NaN, so
Settings › Prices showed "✓ 0 coins" and majority-vote reconciliation
had one fewer source than intended.
Now reads body.asset.price_usd (with a top-level fallback in case the
API is ever flattened).
Two follow-ups on 0.8.0's currency picker after a testing pass:
- The network label was a tiny gray suffix next to the wallet name in
the header, so "which network am I on" wasn't obvious at a glance.
Now it's a separate row of one clickable chip under the coin ticker;
clicking jumps into the browse:chain view where the network strip
actually switches network.
- Clicking the header opened a "Pick a coin" pane that only listed
coins the user already had a wallet for — a first-time user with a
single BCH wallet would see one row and no way to add more. Now it
shows every supported coin behind a search box; owned coins jump to
the wallet list, unowned coins jump straight into the create-wallet
flow with that coin's network group pre-expanded.
Blockchair's BCH explorer is slow and ad-heavy; bchexplorer.cash is
the Bitcoin Cash community's own instance, faster on tx pages and
with a proper mempool view. Same /tx/ + /address/ path scheme
(address takes the bitcoincash: prefix as-is), so no other code has
to change.
Chipnet explorer stays on chipnet.imaginary.cash — bchexplorer.cash
is mainnet-only.
Settings grew tall enough that the user had to scroll past a dozen
cards to reach Prices, Sites or About. Split it into six named
sections (Security, Session, Wallet, Prices, Sites, About) with a
chip nav row at the top; only one section is visible at a time and
the choice persists across restarts.
Adds an About card that names the wallet, the aegis.x front-door
site and the silentmode.st umbrella, so support triage has a
one-click way to reach either from within the panel.
Includes the accumulated 0.7.x-0.8.1 wallet work that was already
shipping on OTA (CashTokens/BCMR, imported-wallet spend, siascan
integration, consolidate, currency picker, footer update chip).
Theseus core:
- addons-host: manifest.category ("plugin") propagates through snapshot(); new
addon API surface checkAndStageSelfUpdate() + restartApp() so a plug-in
can offer in-panel "update now → restart to apply" without pushing the
user to Settings.
- main.js: wires the two new hooks into the AddonHost constructor.
- settings.html: Extensions listing filters out category==="plugin"; those
add-ons live in Plug-ins instead, single source of truth.
Aegis 0.6.31:
- BTC picker trimmed to Signet only; testnet3 hidden (adapter kept so any
existing wallet still loads).
- Wallet strip groups by chain, not chain:network; ticker gets a ▾ chevron
and a dropdown listing every subnetwork with its own totals. Mainnet
reads as the plain ticker; testnets carry a small Chipnet/Signet/Sepolia
pill inline.
- Per-unit price sits directly under the ticker; amount + fiat mirror on
the right — one glance covers name/price/holding/value.
- + Add and ⋯ More promoted from the strip into the header's action row,
next to the new ✎ chip (was the redundant top ⋯). Duplicate "Manage
current wallet" entry removed from the More menu.
- Footer update chip is a two-step flow via the new API: stage → restart.
Falls back to opening Settings on any Theseus that lacks the hooks.
- Manifest declares "category": "plugin".
Rolling every user report from the 0.6.3 rollout into one bundle:
Sounds — the Web-Audio synth palette matches the metaphor now:
- Screenshot: Polaroid shutter — sharp metallic tick + curtain-close click
chained to a film-advance whir (band-passed noise sweeping 900→400 Hz).
- Copy: printer "chika-chika-chika" — three descending percussive noise
bursts pinned by short sine ticks. Reads as a print-head sweep.
- Discard: paper crumple — three overlapping band-limited noise beds
with per-sample random-amplitude crackle, descending centre freq. No
more descending sine "boop".
- Save: soft "photo dispensing" hiss (Polaroid ejects) + a small click.
- Both the panel and editor share the design so nothing sounds different
depending on which surface fired it.
UI polish:
- Discard button now carries a trash-can icon so it's obviously not the
same as the close-sidebar X (they both used to be plain X's).
- Toolbar drawing tools centre themselves via a new .tool-cluster
wrapper (flex:1 1 auto, justify-content:center); the Copy/Save actions
stay right-anchored via margin-left:auto on their own tgroup. Fixes
the maximized-sidebar case where the drawing groups all crowded the
left with a big empty gap before Copy/Save on the right.
- Filename moves out of the topbar into a dedicated footer strip under
the canvas board, alongside a new "Open in folder" button. The topbar
is now flex-wrap:nowrap and holds only fixed-width window controls,
so a long filename can never push discard / sound / max / close onto
a second row (the filename ellipsises instead).
- "Open in folder" invokes a new "openFolder" addon message that calls
Electron's shell.showItemInFolder() to open the OS file explorer with
the specific scratch PNG highlighted (falls back to shell.openPath()
on the scratch dir when no capture is named).
Version bump so the OTA update endpoint picks it up on the next tick.
Four issues from the user's report on 0.6.2:
- Recent captures had a global "clear all" but no way to drop a single
screenshot. Each tile now grows a small × button (visible on hover;
drops in behind the thumbnail preview so it never obstructs the
content). Clicking the × invokes clearRecent({name}) and removes both
the ring entry and the scratch PNG on disk. Bubble-guarded so the ×
click doesn't also trigger the tile's "load into preview" handler.
- Select tool button removed — clicking it did nothing visible, so users
read it as broken. The internal "select" mode still exists as the
no-tool state; you get back to it now by clicking the same drawing
tool a second time (toggle-off) or hitting Escape. The active-drawing-
tool button flips its border when armed.
- Text tool made unmistakable: input paints with a 2 px acid border, a
glowing acid halo, dark background, and the visible ink colour on the
text itself. Focus attempt is three-layered (sync, rAF, timer) to
outrun any Chromium build that drops the mid-pointer-event focus. Non-
Enter/Escape keys get stopPropagation so a stray document listener
can't steal the focus mid-typing.
- Panel header's sound / max / close cluster kept nudging inward when
the status text was empty. The parent's `justify-content: space-
between` distributed the row unevenly. Force-anchor the cluster with
`#btn-sound { margin-left: auto }` so the three window-control icons
hug the right edge regardless of what fills the middle.
Bundled but not shipped separately — parent session signs and pushes.
Layout reorganisation from user's diagram:
- Copy + Save move out of the topbar into the toolbar as their own
right-anchored tgroup (margin-left:auto). On wide sidebars they sit at
the end of the drawing-tool row; when the sidebar is narrow, the
actions cluster wraps as its own row on the right instead of nudging
the drawing tools around. Toolbar switches from justify-content:center
to flex-start so the leading tool groups pack left and the actions
group can find the right edge cleanly.
- Topbar right cluster is now Discard / Sound / Maximize / Close — Copy
and Save are gone from the topbar entirely so the right edge reads
as controls-only, not action-mixed.
- Discard button (X icon, danger red on hover) throws away the current
capture — silentmode.invoke("clearRecent", {name}) removes it from the
ring and unlinks the scratch file — then navigates back to the panel.
Distinct from Back, which is non-destructive.
- Close button already existed from 0.6.1 but stays in the same
right-edge position for continuity.
Bundled but not shipped separately — parent session signs and pushes.
Users saw a blank window with a white strip across the top for seconds
on launch. Root cause: every part of startup, including session restore,
waited for chrome.html's did-finish-load. That event also waits for the
page's subresources, and the bookmarks bar loads its favicons over
bns:// — a BNS lookup plus a network fetch each — so a slow link held the
whole boot. On top of that, seven hidden overlay renderers, every restored
tab, the BNS index build and three network fetches all started in the
same tick and stalled the main thread ~1 s while the toolbar tried to
paint.
- Continue boot at chrome.html's dom-ready (toolbar scripts have run, IPC
listeners exist) instead of did-finish-load; 8 s fallback timer.
- Window and chrome view get the toolbar's --bg for the active theme so
the pre-paint frame is never white.
- Overlay pages (site info, engine picker, downloads, suggestions,
password fill, link status, approval) load 250 ms after the toolbar or
on first use; the approval modal awaits its page so a dapp request
can't hang.
- Session restore is staggered: active tab first, then one background
tab per 150 ms slotted into its saved strip position. Session file v2
records the active index; v1 arrays still load (active = last, as the
old loop effectively did).
- AddonHost gains api.whenUiReady(); Aegis 0.6.2 defers its heavy
dependency loading (noble precompute, bitcoinjs, libauth, WizardConnect)
behind it.
- BNS snapshot warm-up still starts right after createWindow (bookmark
favicons need it); Sia refresh, update check and home-card fetch move
to the post-paint phase.
Measured on a clone of the real profile with nine restored tabs: toolbar
usable at ~0.7 s instead of ~1.5 s, main-thread stall during toolbar load
down from ~1.1 s to ~0.2 s.
Four user reports from the 0.6.0 rollout:
- Text tool never committed. openTextInput placed the box correctly but
a couple of Chromium quirks stopped a normal type-Enter cycle:
focus() called synchronously right after appendChild lost the race
in some builds, and the input's own mousedown / click was bubbling
through to #base and re-firing openTextInput on every subsequent
keystroke click-through, so what looked like "nothing happens" was
actually "a new empty box spawned on top of the last one every time".
Now: focus after requestAnimationFrame, contain pointerdown / mousedown
/ click inside the input so they don't bubble to the canvas, track
the font size on the state so commit uses the same one openTextInput
measured against, and preventDefault on the base pointerdown so
Chromium doesn't reset focus back to <body>.
- Toolbar wrapped one dot at a time when the sidebar was narrow (a
lonely thin/medium/thick width would jump to a second row while the
swatches stayed above it). Toolbar items are now wrapped in
`<div class="tgroup">` per category — tools / swatches / widths /
undo-redo — with `flex: 0 0 auto`, so a whole row wraps as a unit
and lands cleanly under the previous one. `gap: 10px / row-gap: 6px`
keeps the visual grouping obvious.
- No way to close the sidebar without hunting for the dock icon. Added
an X button in the top-right of both the sidebar panel and the
editor toolbar. Both wire through a new `silentmode.sidebar.close()`
preload method that calls the existing `sidebar-close` IPC.
- Tightened the pointerdown text branch so preventDefault + explicit
focus-after-frame make the click-through races impossible.
Bundled but not shipped separately — parent session signs and pushes.
Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
Editor:
- Crop tool restored — drag to select, marquee sits with a dashed acid
border and a dimmed backdrop for the area you'll discard, then the
topbar shows Apply crop / Cancel. Applying trims #base to the rect,
resets undo (dimensions changed), and drops back into the select tool.
Enter / Esc keyboard shortcuts while a crop is pending.
- Blur / mosaic redaction tool back — drag a rectangle, editor
downsamples that region of #base to ~12-block granularity and paints
the blocks back nearest-neighbour. Commits directly (no confirm step).
- Sidebar-window controls (Back, name, Copy, Save, Sound, Maximize)
reflow: Back + name on the left, Copy + Save + Sound + Maximize on
the right so the "put the sidebar back to normal size" affordance
lives where users expect it. Toolbar's drawing tools stay centred.
- Back arrow icon swapped from a chevron to a proper flat arrow
(line + arrowhead), matching the new browser back/forward glyphs.
Sounds — modeled on Firefox Screenshots' feedback rather than beeps:
- Shutter is now a real photoshoot click: two mirror-slaps built from a
band-passed noise burst (metallic ping) plus a very short square-wave
thud each. Sounds like a camera, not a beep.
- Copy is a two-chirp "printer feed" — filtered noise burst on top of a
sine chirp per beat, staccato ascending pair. Same shape Firefox Easy
Screenshot uses for "copied to clipboard".
- Save keeps its ascending triad; Discard keeps its descending pair;
new small ascending pair for Apply crop.
Chrome:
- Browser Back / Forward chevrons (M10 3 L5 8 L10 13 — two segments
meeting at a point, no shaft) replaced with straight-arrow glyphs
(line + arrowhead). Reads as a navigation arrow, not an angle bracket.
Bundled but not shipped separately — parent session OTA-signs and pushes.
Two things the shipped 0.5.0 got wrong:
- `.empty { display: flex }` overrode the plain `[hidden]` attribute the
init flow sets after the image draws, so the "Loading capture…" pill
stayed visible on top of the finished capture. Global rule
`[hidden] { display: none !important }` takes it out.
- Tool bar was left-aligned; older editor iterations grouped the drawing
tools / swatches / widths / undo-redo in the centre of the bar, which
read better in a narrow sidebar. Adds `.toolbar { justify-content: center }`;
the topbar's back / max / sound / name / save / copy stay edge-anchored.
Version bump so the OTA update endpoint picks it up on the next tick.
Two problems the old editor kept hitting:
- __pending drain race: opening the editor a second time (refresh, back-and-
forth navigation) found the storage entry already consumed and bailed to
a blank canvas silently.
- Cross-origin img loading: editor.html at file:///…/addons/screenshot/
loading a scratch PNG at file:///…/addons-data/ counts as cross-origin
under Chromium's file-URL policy; setting crossOrigin="anonymous" made
the load fail outright.
Rebuilt editor v2:
- Load path is idempotent: silentmode.invoke("getBytes", {name}) → addon
reads the scratch file and returns a data URL. No __pending drain, no
cross-origin trickery — data: URLs are same-origin and never taint the
canvas, so getImageData / toBlob keep working.
- Two-canvas model (#base + #over, over is pointer-events:none) so live
previews don't cost a full re-composite per mousemove.
- Tools: cursor, arrow, rect, ellipse, pen, text. 6 swatches, 3 widths,
undo / redo (25-deep). Copy + Save at the top bar. Back and Maximize
buttons in the same top bar so navigation controls stay reachable when
the toolbar wraps at narrow widths.
- Keyboard: A/R/O/P/T select tool, Esc = cursor, Ctrl+Z/Shift+Z undo/redo,
Ctrl+S save, Ctrl+C copy.
- Toast surface for save/copy/error feedback.
Sidebar panel gains a direct raw-save path so the user can copy or save the
capture without entering the editor:
- Two-row actions: [Copy] [Save] on top, [Discard] [Edit] below.
- Copy uses navigator.clipboard.write(ClipboardItem); Save uses
<a download> with a Blob URL — same path Chromium's will-download
tracker already handles, so the file lands in Downloads and the chip
updates like any other save.
Inline "clear all" confirmation replaces the native confirm() — the old
system-modal opened over the tab area (out of the sidebar's visual
context) and looked like Windows 95. Now a compact red strip appears
under the Recent header with Cancel / Delete buttons.
Sounds + a sound-on/off toggle in both surfaces:
- Web Audio oscillator-synthesized (no .wav shipped): shutter click on
capture, two-tone bloop on copy, descending pair on discard/back,
ascending triad on save.
- Preference stored in silentmode.storage under "soundOn" (default on),
shared between the panel and the editor.
Simplifications:
- Dropped the addon's "arm" onMessage handler (superseded by getBytes).
- Manifest capabilities: sidebar-panel + capture-tab (no open-tab,
no toolbar-menu).
Bundled but not shipped — parent session handles the OTA sign + push.
Replace the hand-drawn approximations with the official SVGs from
github.com/spothq/cryptocurrency-icons — the permissive-licensed set most
exchanges, block explorers, and other wallets standardised on. Users see
the same BCH / BTC / DGB / SC / TRX / ETH / SOL marks in Aegis they
already recognise from Coinmarketcap, Coingecko, Trezor, MetaMask, etc.
- BCH: green disc with the tilted Bitcoin-Cash B
- BTC: orange disc with the classic Bitcoin B glyph
- DGB: blue disc with the DigiByte D + swash
- SC: brand-green disc with Siacoin's stylised S
- TRX: red disc with the geometric Tron triangle-net
- ETH: purple disc with the two-triangle Ethereum rhombus
- SOL: mint disc with the three-slash Solana mark
All SVGs are inlined in panel.js — no network fetches at panel load.
Bumped addon 0.4.3 → 0.4.4 so seedBundledAddons reseeds the new panel
on next launch.
The toolbar dock still showed the 🛡 emoji even after chrome.html learned
to render data-URI icons — because registerSidebarPanel({icon}) is the
per-panel icon that overrides manifest.icon, and Aegis was passing "🛡"
verbatim. Dropping the override lets addons-host's `icon = manifest.icon`
default kick in, so the dock button pulls the branded aegis.x/brand
shield the manifest now advertises.
Version bumped 0.4.1 → 0.4.3 to force seedBundledAddons to reseed the
new index.js on next launch.
User report: the sidebar preview lands correctly, but the moment the editor
opens in its own tab the picture is blank. Rather than chase that class of
handoff race again, put the editor in the same webContents as the panel:
the sidebar view navigates panel.html ↔ editor.html in place. Same
document object, same silentmode.storage surface, no cross-tab __pending
transfer at all.
- panel.html "Edit" button now calls silentmode.invoke("arm", …) — the
add-on rewrites __pending with the currently-previewed capture's bytes,
and the panel does location.href = "editor.html?name=…". Sidebar view
loads the editor with the same preload; editor.js's storage-based load
path pulls the pending entry out and paints.
- editor.html gains a "Back" arrow (returns to panel.html) and a
maximize / restore icon.
- discard() now navigates to panel.html instead of closeTab() — there is
no tab to close.
- Manifest drops the "open-tab" capability entirely (no more full-tab
editor); keeps sidebar-panel + capture-tab.
Framework: new silentmode.sidebar.{maximize, restore, toggleMax, isMax,
onMaxChange}. main.js honours them via new sidebar-maximize / -restore /
-toggle-max / -is-max IPCs, remembering the pre-maximize width so a
restore drops back exactly. The sidebar drag-grip auto-exits maximize
mode on any user drag, so pulling the edge always lands on the pre-max
value plus/minus the delta. sidebar-preload exposes the surface;
chrome.html renderer is untouched — this is a per-panel affordance.
Editor tools (crop / arrow / rect / ellipse / pen / text / mosaic /
undo / redo / copy / save) unchanged. Save still goes through Chromium's
<a download> path, so the file lands in Downloads and appears in the
download chip like any other save.
Bundled but not shipped — leaving version bump + deploy to parent session.
0.3.33 still ships blank screenshots because the whole toolbar-menu → auto-
open-editor path can't be made race-free: the moment the editor tab opens
it becomes the active tab, and a snapshot of the editor's own tab (before
its canvas has drawn from storage) is a valid-looking 24 KB all-white PNG.
The lastCapturableTabId fallback I added in a279864 catches the second
click, but the first click can still land on the addon-owned tab whenever
the user re-triggers before setActive has settled.
Rebuild the UX so this class of race can't happen at all:
- Drop the toolbar-menu capability. Manifest is back to sidebar-panel +
capture-tab + open-tab, so the dock icon opens the panel (never the
editor directly). No dropdown, no clip-under-tab-view issue, no auto-
jump into an addon-owned tab.
- Sidebar has the three capture buttons + a preview <img> + a "Open in
editor tab" button. The preview is fed a data:image/png URL returned
straight from api.captureTab, rendered inside the sidebar's own
document — same origin, no file:// cross-directory gotcha, and the user
can see immediately whether the shot actually landed.
- Editor.html tab opens only on an explicit "Open in editor tab" click.
The addon rewrites __pending at that moment (so the editor always sees
the just-selected capture even if a prior editor tab drained the entry),
then api.openTab("editor.html", {name}). The editor's storage-based
load path is unchanged.
- Recent captures ring is kept and now exposed as a horizontal thumbnail
strip in the sidebar; clicking a tile re-previews that capture and
arms "Open in editor tab" for it.
Editor page (editor.html/js/css) unchanged — same crop / arrow / rect /
ellipse / pen / text / mosaic-redact / undo / redo / copy / save.
Bundled but not shipped — leaving version bump + deploy to the parent
session.
Two follow-ups from the on-device test.
Two Aegis addons showing up (bchwallet + aegis):
- migrateAegisRename previously only ran when addons/aegis/ didn't exist,
which meant any bchwallet copy the signed OTA update endpoint reinstalls
after the first migration stays there forever, and AddonHost loads both
as separate wallets. Rewritten to always retire addons/bchwallet/ when
it's present, regardless of whether aegis/ is already installed. The
storage-copy (bchwallet.json → aegis.json) still only runs the first
time so a downgrade doesn't clobber fresh 0.4+ state.
- Also flushes any stray addons/siawallet/ that comes back the same way.
Sidebar dock and Extensions list icons showed the raw 🛡 emoji:
- chrome.html's dock-button renderer and settings.html's extensions-list
renderer now accept `data:image/svg+xml…` values for manifest.icon and
render them as <img> instead of text. Emoji strings still render as
before.
- aegis addon.json's icon is now the exact hex-aspis mark from
aegis.x/brand/favicon.svg (URL-encoded inline). Version bumped to
0.4.1 so seedBundledAddons reseeds the new addon.json on next launch.
Cleans up the naming that leaked from the wallet's origin story (BCH-only)
into the actual bundle layout. Aegis is one integrated addon now:
- Bundle folder: TheseusNavigator/bundled-addons/aegis/ (was bchwallet/).
- Addon id: "aegis" (was "bchwallet"). Vault-derive still accepts
legacy "bchwallet/*" and "siawallet/*" paths via the
absorbs list, so no on-chain funds move.
- Version: 0.4.0 (bumped to trigger seedBundledAddons's reseed).
- Retired: TheseusNavigator/bundled-addons/siawallet/. Sia is
folded into Aegis as a chain adapter (lib/sia/*.js
already in-tree) and Aegis's manifest lists siawallet
under absorbs so pre-Aegis SC keys derive identically.
main.js migrateAegisRename() runs before seedBundledAddons on every
launch. First run does the move; subsequent runs are no-ops:
- addons/bchwallet/ -> addons-backups/bchwallet-migrated-<stamp>/
- addons-data/bchwallet.json COPIED to addons-data/aegis.json (kept
copied not moved so a downgrade to 0.3.x can still boot).
- addons/siawallet/ -> addons-backups/siawallet-migrated-<stamp>/
(addons-data/siawallet.json left untouched — its walletdUrl is
per-user config Aegis's Sia wallet takes fresh via Settings).
settings.html Aegis update card now matches either "aegis" (new id) or
"bchwallet" (pre-rename) so upgraders coming from 0.3.x see the same
one card while the OTA endpoint's next signed bundle catches up.
Internal purpose paths inside index.js/chain-*.js are unchanged —
LEGACY_BCH_PURPOSE stays "bchwallet/mainnet/0" and every purposePrefix
still starts with "bchwallet/*". The addon absorbs its own former id,
so those paths keep resolving to the same seed the shipping Aegis has
been using since 0.3.14.
Setup 5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643
Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4
Bundled since 0.3.27:
32d4b75 - Aegis (bchwallet) gains its own update card in Settings >
General beside Ariadne. Check for updates hits the same signed OTA
endpoint the boot timer uses; Restart to apply appears when a signed
newer version is staged. Uses the existing addons-check-updates + a
new app-restart IPC. New Aegis versions ship without a Theseus release.
32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to
the right of the tab (mode: 'right') instead of a detached window.
Matches stock Chrome. Users who prefer detached can drag out via the
DevTools own toolbar.
b71c925 - Search-engine favicons in Settings > Search now use Google's
/s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough
hosts (Brave, Bing, Yandex, etc.) that half the list was falling
through to the emoji placeholder.
Deployed. Verified LIVE 0.3.28.
Two fixes off the first-launch feedback: users didn't see how to add a
wallet, and the branded shield from aegis.x/brand hadn't landed in the
panel.
- panel.html + panel.js: swap the ad-hoc shield SVG for the exact mark
from aegis.x/brand/favicon.svg — hexagonal aspis with dark fill +
acid stroke + boss ring + centre point. The panel's tab favicon
(<link rel="icon">) and the "Aegis" fallback badge in the header
now render byte-close to what a user downloads from the brand kit.
- Add-wallet discoverability: an always-visible "+" chip lives in the
header next to the picker caret; clicking it opens the picker with
the coin list pre-expanded. When the panel is genuinely empty (a
vault Aegis hasn't seen before), the gate now shows a big primary
"+ Add your first wallet" button plus copy that spells out the
seed source — Aegis derives every wallet from the Theseus password
vault, no separate seed to import.
- addon.json bumped to 0.3.1 so seedBundledAddons() picks up the fresh
panel files on the next Theseus launch (bundleVer === userVer would
otherwise skip the reseed and users would keep loading the old
panel from their addons/ dir).
Blank editor + broken buttons root cause: index.js was writing the
capture to <userData>/addons-data/screenshot-scratch/<name>.png and
passing "?src=file://<that path>" to editor.html. The editor lives at
file:///<userData>/addons/screenshot/editor.html — different directory
tree under file://. Chromium's file:// origin policy treats those as
different origins and quietly refuses the <img> load, so init()'s
loadImage() rejects, the canvas never gets an image, and every tool
after that operates on a still-empty 300×150 default canvas — the
tools appear to work but produce no visible output because the base
image never landed. The sidebar version we replaced set
`previewImg.src = dataUrl` (a base64 data URL) directly, which has no
origin and just worked; the tab version regressed by adding the file
hop.
Fix keeps the scratch file for the recent-captures ring but hands
the raw capture through the add-on's per-add-on kv store
(`__pending` key). Same store, same origin scoping, no
cross-directory read: index.js writes via api.storage.set from main;
editor.js reads via window.silentmode.storage.get through the tab
preload (packaged since 0.3.27). Fallback path retained for
"openRecent" callers still passing ?src=… — those will need their
own fix in a follow-up.
Bumped to 0.2.4 and signed for the OTA endpoint — first real
independent add-on ship: no Theseus release needed to fix this,
0.3.27 installs pick up 0.2.4 via the boot-time signed-update poll.
Three tied-together fixes:
1) captureTab moves from WebContents.capturePage() to CDP
Page.captureScreenshot for every mode (visible / full / region).
Blank-screenshot symptom: after a toolbar-menu selection, the OS
popup teardown left the tab view marked occluded for a few frames
on some Windows setups, so capturePage() snapshotted a
stale/transparent frame at the correct dimensions — no 0x0, no
retry hit. CDP forces a fresh composite regardless of occlusion
state (same path the "Full page" mode was already using) and
returns a base64 PNG directly; PNG dimensions come out of the
IHDR chunk (bytes 16-24). Attach only when nothing else has, and
detach after only if WE attached, so an open DevTools stays
attached.
2) Editor gets a Discard button. Toolbar picks up an "×" glyph next
to Save/Copy that closes the editor tab and drops the working
screenshot. Top-level Escape now falls through the same path
after unwinding an in-flight text placement or crop rectangle. A
new "addon-tab-close" IPC lets an add-on's own tab close itself
(main matches the sender's webContents id against the tab list,
so a page can only close its own tab); window.silentmode.closeTab()
exposes it from addon-tab-preload.js.
3) Manual update controls in Settings > Extensions. New "Check for
updates" button at the top of the Extensions surface calls the
same signed-update polling the boot timer runs; the result is
surfaced inline ("All extensions are up to date" / "N updates
staged; restart Theseus to apply"). A "Pending updates" box
below lists what's in <userData>/addons-updates-staged/ so the
user knows what will be promoted on next restart.
Toolbar-menu popup settle bumped from 120 ms to 250 ms with an
explicit win.focus() in the popup close callback — the previous
window wasn't enough on slower Windows setups. CDP capture no longer
depends on this delay anyway, but the settle still helps any add-on
that does DOM work in its click handler before capture.
Screenshot add-on bumped 0.2.2 → 0.2.3 (Discard button; capture
fixes come from the host, not the add-on).
Two follow-ups on the light-mode acid work:
1) Every hardcoded #d6ff3d and rgba(214,255,61,X) in the browser
chrome and every addon panel now goes through var(--acid), so the
light-mode BCH-teal (#0AC18E) takes effect everywhere — not just
where var(--acid) was already used. Hex-with-alpha (#d6ff3d55 etc.)
converts to color-mix(); rgba() converts to rgb(from var(--acid)…)
for the same alpha with the current --acid hue. Chromium 128+
supports both. Files touched: chrome / settings / error / home /
approval / bchwallet / siawallet / screenshot (html + css).
Screenshot editor.js's #d6ff3d stays — that's the drawing colour
swatch, not UI chrome.
2) The Theseus button (.logo) and update chip (.upchip) become dark
BCH-navy chips (#253A49 background, #F8FDFF text) in light mode.
Previously the .logo hardcoded #d6ff3d text on a bright-acid tint —
invisible on a light toolbar. The dark chip stands out and gives
the light theme a distinct accent using the BCH secondary from
whybitcoincash.com's palette.
Two related visibility fixes:
1) Light-mode --acid → #0AC18E (Bitcoin Cash brand primary, from
whybitcoincash.com's palette per user). Direct swap from #088A66
(darkened variant) to the on-brand primary. Applied across chrome /
settings / error / home / approval / messages / bchwallet /
siawallet / screenshot editor. Dark mode's #d6ff3d is unchanged.
2) User-Agent no longer includes 'theseus-navigator/<ver>' or
'Electron/<ver>' tokens. Cloudflare's WAF was returning HTTP 503
'Service Unavailable' to any request carrying those (verified
directly against whybitcoincash.com — same URL, same headers, only
the UA differed; plain Chrome UA got 200, Theseus UA got 503).
Strip both tokens via a stockChromeUA() helper called from
applyAcceptLanguage(), which whenReady already invokes at boot.
Standard practice: Brave, Vivaldi, Slack all do the same.
Verified via CDP: navigator.userAgent now reports
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/130.0.6723.191 Safari/537.36
— indistinguishable from stock Chrome.
Prior light-mode --acid was #3a5c00 (dark olive-green) — legible but
off-brand. The Bitcoin Cash brand primary is #0AC18E (a teal-leaning
green already used in bchwallet's --bch variable). Darken it a step to
#088A66 for AA text contrast on white (~5:1) while staying in the BCH
family — the light-mode accent now reads as "Bitcoin Cash green,
darkened for legibility" instead of an arbitrary olive.
Applied across every chrome page + addon panel that carries the light
override (chrome / settings / error / home / approval / messages /
bchwallet / siawallet / screenshot editor). Dark mode's #d6ff3d
untouched.
Bundled screenshot addon bump:
- version 0.2.1 → 0.2.2
- palette grows from 5 to 8 colors: adds Orange (#ff9500), Blue
(#0a84ff), Purple (#bf5af2) alongside acid/red/yellow/white/black —
common annotation colors that were conspicuously missing
- updateURL swings from the aspirational addons.silentmode.st (which
never resolved) to the live gateway URL
https://navigate.st/bns/theseus.x/extensions/screenshot/updates.json,
where the operator's first signed update entry is now published
The gateway URL is deliberate over the bare `theseus.x/...` form: the
add-on updater runs from Node's main-process https module, which uses
the OS resolver. On installs without Ariadne's Thread the OS can't
resolve theseus.x (BNS-only TLD), so the poll would silently fail;
the navigate.st gateway resolves via standard DNS and forwards to the
same BNS-backed Sia content, so every install reaches the endpoint.
First signed update is live at:
https://navigate.st/bns/theseus.x/extensions/screenshot/updates.jsonhttps://navigate.st/bns/theseus.x/extensions/screenshot/screenshot-0.2.2.tar.gz
signed 59a35370fdbc9d1e24834fa26c7765d27e8763fe928bfa23b202ca666a6a6973
by the ops key baked into 0.3.19. End-to-end verified via
scratchpad/decoupling-test/verify-live.mjs against the live endpoint:
fetch, sig-verify, download, sha-verify, extract, stage, promote,
backup — all pass.
Installs polling the previous updateURL (addons.silentmode.st) get
this new URL only after their bundled copy is refreshed to 0.2.2,
which means either a Theseus release with 0.2.2 bundled (0.3.20+) or
a signed update at the old URL that carries the URL change (impossible
because addons.silentmode.st doesn't resolve). Ship a Theseus release
that bundles this 0.2.2 to activate the update path on existing
installs; from then on the endpoint self-perpetuates via the theseus.x
URL.
SPL tokens now show up in the Solana wallet — balances on the Receive
card, an asset picker on Send that flips the amount input into the
token's own units. Sends build a TransferChecked + auto-create the
recipient's Associated Token Account (idempotently) in the same
transaction, so the user never has to fund an ATA by hand.
- lib/sol-spl.js: SPL primitives that don't need @solana/web3.js.
TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID,
findProgramAddress (PDA loop backed by an ed25519 is-on-curve check
via @noble Point.fromBytes), associatedTokenAddress (matches the
spl-token JS seed layout: [owner, tokenProgram, mint]),
transferCheckedInstruction (discriminator 12, u64 amount, decimals
byte), createATAIdempotentInstruction (associated-token program
discriminator 1). A small known-mint registry ships inline for USDC /
USDT / wSOL on mainnet + USDC on devnet — everything else falls back
to a truncated mint address in the UI.
- Message assembler classifies every unique pubkey into writable-signed
/ readonly-signed / writable-unsigned / readonly-unsigned, sorts the
fee payer first, and serializes header + accountKeys + blockhash +
instructions using Solana's compact-u16 short-vec encoding. Same
wire shape @solana/web3.js produces from Transaction.serializeMessage.
- lib/chain-sol.js: snapshot() now carries a tokens[] array of
{mint, symbol, name, decimals, balance, tokenAccount, tokenProgram,
isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against
both the classic Token program and Token-2022. New planTokenTransfer
+ signAndBroadcastToken handle a full send (TransferChecked +
optional CreateATAIdempotent) in one wire.
- Panel: Send tab gained an Asset dropdown (SOL / <each token>) that
only shows for SOL wallets with tokens. Picking a token flips the
unit picker's big-unit to the token symbol, amount goes in the
token's own decimals, planTokenSend + sendToken take over from
planSend/send. Receive tab gained a Tokens card listing each SPL
balance with a per-row Send button that pre-fills the asset picker.
- Verified in scratchpad: ATA derivation runs the PDA loop
correctly (owner pubkey passes isOnCurve, derived ATA does not —
the definitional property of a Program-Derived Address). Cross-check
the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS
and the value matches.
Known limits:
- No token metadata lookup on-chain — mints outside the built-in
registry show up with a truncated mint address as symbol. Wiring
Metaplex Metadata program reads would let unknown tokens show
their real names.
- Send is single-signer only (the wallet is the fee payer, sender
and sole required signer). Multi-sig SPL transfers work via the
dapp bridge (window.solana.signAndSendTransaction, which already
handles partial signatures).
Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp
that wants to route through Polygon (or Base, or Arbitrum, or any other
EVM the Silent Mode user hasn't added yet) calls the pair the industry
already wrote for it — Aegis registers the chain, provisions a wallet on
it under the same vault seed, auto-connects the origin, fires
chainChanged, and hands the dapp back a provider pointed at the new
chain. No sidebar detour, no Custom RPC copy-paste. Users still see
every chain in the picker post-add and can revoke sites in Settings.
- lib/chain-eth.js: EthWallet accepts a customNetwork override
({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}).
When present it replaces the NETWORKS lookup so mainnet+Sepolia
ship built-in and every EIP-3085 chain is a runtime override the
addon persists. The ticker flows into snapshot() so the send
approval reads MATIC / BNB / whatever the chain's native currency is,
not a hardcoded ETH.
- index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl,
explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`.
Persisted under api.storage.customEthChains, so an added chain
survives Theseus restarts. chainMeta("eth", "custom-<chainId>")
synthesizes the meta from storage so the panel renders custom
chains without needing them in COINS at module-load time.
- eth.addChain handler (EIP-3085): approval overlay shows chain
name, decimal + hex chain id, native ticker, RPC and explorer
URLs (the phishing-signal quartet). On approval, persist config +
create wallet with a custom-<chainId> network + auto-grant the
origin readAddress on this chain. No-op success if the chain is
already added.
- eth.switchChain rewritten to be EIP-3326 correct: look up any
ready ETH wallet whose adapter reports the requested chainId,
make it the selected wallet, fire chainChanged. When no wallet
matches, throw with .code = 4902 (the standard 'chain not
added' code) so wagmi / RainbowKit / any 3326-aware dapp does
the fallback wallet_addEthereumChain call in the same click.
- eth.state handler: cheap {address, chainIdHex, networkVersion}
peek for the origin's currently-connected wallet (no approval,
no key access). The main-world bridge calls it after every
switch/add to emit chainChanged + accountsChanged locally — the
events MetaMask fires and RainbowKit listens for.
- wallet-inject.js: routes wallet_addEthereumChain via
eth.addChain, preserves the 4902 code across the postMessage
boundary on switch failures, calls pullEthStateAndEmit() to fire
the post-switch/add events.
Two follow-ups to the dapp bridges. Both change wire shape only — no new
UI, existing wallets keep signing byte-identically for the flows they
already covered.
- lib/eip712.js: full EIP-712 typed-data encoder — encodeType with
alphabetically-sorted transitive sub-types, typeHash, encodeValue for
string / address / bool / uint*/int* (any width) / bytes / bytesN /
nested structs / dynamic and fixed arrays, hashStruct recursion,
digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct).
Verified against the spec §"Ether Mail" test vector — hashStruct on
both the domain and the message plus the final digest all match the
canonical values byte-for-byte (see scratchpad/verify-eip712.mjs).
- chain-eth.js: exposes signTypedDataDigest(digest32) that signs the
precomputed digest with r||s||v (v = 27+recid), the same envelope
personal_sign uses. Aegis computes the digest server-side (in the
addon) so a bug in the encoder can't be tricked by a malicious dapp
into signing over data the user never saw.
- index.js: eth.signTypedData handler shows domain (name · version ·
chainId), primary type, and a truncated JSON preview of the message
in the approval overlay — every classic phishing signal (mismatched
domain, unexpected primary type) is in front of the user before they
hit Sign. Accepts either an already-parsed typedData object or the
JSON-string form older MetaMask specs used.
- wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same
payload shape) route to eth.signTypedData. v1's flat "type[]" form
is unwired — dapps that still use v1 should upgrade.
- Solana signAndSend: bridge now passes the FULL wire (from
tx.serialize({requireAllSignatures:false, verifySignatures:false}))
instead of just the message. The addon parses compact-u16 signature
count, finds this wallet's pubkey in the message's account-key list,
signs the message, and patches ONLY its own slot in the signature
array — any partial signatures the dapp had already filled with
tx.partialSign() (session keys, escrow co-signers, permissioned
authorities) are preserved. Multi-signer flows work now; single-signer
is the degenerate case of sigCount=1.
- Approval overlay for sol.signAndSend now shows required-signer count
and the wallet's slot index so multi-signer requests are visibly
distinct from a plain single-signer send.
Aegis now integrates with the two dapp-wallet APIs the wider ecosystem
actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style
window.solana for Solana — plus BTC signet as a third Bitcoin network
alongside mainnet + testnet3.
- wallet-inject.js: adds a main-world bridge, installed via a one-shot
<script textContent=…> appended to <head> and immediately removed.
Electron's contextBridge shallow-copies args and strips methods, which
means BCH- and Tron-shaped params (plain data) work in the isolated
world but Solana's wallet-adapter dapps — which pass @solana/web3.js
Transaction objects and expect .serializeMessage()/.addSignature() to
fire on them — need code that lives in the same world as the dapp.
Bridge talks back to the isolated world via window.postMessage on a
namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to
theseus.invoke. Same pattern MetaMask + Phantom use.
- window.ethereum (EIP-1193): request({method, params}), on(),
removeListener(), chainId, networkVersion, selectedAddress. Handles
eth_requestAccounts, eth_accounts, eth_chainId, net_version,
personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain
(rejects with "use the Aegis picker"), wallet_addEthereumChain
(rejects, chains come from Settings), wallet_get/requestPermissions.
Every other eth_*/net_*/web3_* method passes through to the wallet's
configured RPC via a new eth.rpc handler. EIP-6963 announceProvider
event fires so wagmi / RainbowKit / any 6963-aware dapp discovers
Aegis alongside MetaMask instead of racing for window.ethereum.
- window.solana (wallet-adapter shape): connect(), disconnect(),
publicKey (with toString/toBase58/toBytes/equals — the PublicKey
interface dapps check), signMessage(u8) → {publicKey, signature: u8},
signTransaction(tx) → mutates + returns the same tx with the
signature added, signAndSendTransaction(tx) → returns {signature: txid},
signAllTransactions([tx]), request({method, params}). isPhantom flag
set true so dapps that gate on it pick us. on/off events for connect
/ disconnect / accountChanged.
- Handlers in index.js registerPageMessages: eth.requestAccounts,
eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc,
sol.connect, sol.signMessage, sol.signAndSend. Every write path is
per-origin gated + goes through api.approvalModal with the wallet
label + network in the row list so the user always knows which
Aegis wallet is about to sign.
- Signet added to chain-btc.js — signet shares testnet3's address
format and SLIP-44 coin type (BIP-325 only changed consensus/signing),
so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only
the electrum pool (aranguren + wakiyamap) + explorer (mempool.space
/signet) + faucet (signetfaucet.com) differ. Registered as
btc:signet in COINS with per-network coinType lookup.
Known limits (follow-ups in the same shape as existing chains):
- SOL signAndSendTransaction is single-signer only; dapps that combine
the wallet's sig with co-signer sigs need the wire assembled on the
dapp side.
- ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set
covers personal_sign only.
Decouples bundled-add-on updates from Theseus releases. An add-on
whose addon.json declares an updateURL can be republished at any time
without shipping a new Theseus installer; existing installs pick it up
on the next boot's +30 s background check.
Client flow (main-process only, no UI touchpoints in this commit):
initAddons()
├── promoteStagedUpdates() # promote signed stage if newer
├── seedBundledAddons() # bundle wins over on-disk if newer
└── AddonHost.discoverAndActivate()
30 s later:
└── checkAndStageUpdates() # fetch, verify, download, stage
Signature: Ed25519 over
"silentmode.addon-update-v1|<id>|<version>|<tarball-sha256>",
verified against a hardcoded set of operator pubkeys living in
addon-update-pubkeys.js. Domain-separated so the operator key can't
be tricked into signing a message with a different purpose. Empty
pubkey array is the shipping default — checkAndStageUpdates() then
short-circuits and no outbound requests are made, which is the safe
posture until the operator ceremonies a key in.
Payload: gzipped tar, extracted with the system tar (present on
Win10 1803+, macOS, Linux). Path traversal defended by tar's default
refusal of `..` entries; the extracted manifest's id + version are
re-checked against the signed values before staging.
Staged updates go to <userData>/addons-updates-staged/<id>-<version>/.
Promotion into <userData>/addons/<id>/ reuses seedBundledAddons's
backup dance: existing folder moves to
<userData>/addons-backups/<id>-<oldver>-<timestamp>/ so any local
edits survive.
New files:
- addon-updater.js — client
- addon-update-pubkeys.js — hardcoded pubkeys (empty; edit + rebuild to rotate)
- scripts/generate-update-keypair.mjs — one-time keygen
- scripts/sign-addon-update.mjs — operator packager+signer
- docs/ADDON-UPDATES.md — operator brief + threat model
Wired into main.js at boot; screenshot add-on's addon.json advertises
the reference updateURL for when the endpoint goes live.
Aegis's Bitcoin adapter can now sign transactions from every BIP44/49/84/86
address it derives. Receive already worked on all four in the previous rev
— this closes the send side.
- BIP44 (legacy P2PKH, 1…): signAndBroadcast now fetches each spent UTXO's
parent transaction via blockchain.transaction.get(txid, false) and hands
the raw hex to PSBT as nonWitnessUtxo. Prev-tx calls fan out in parallel
with Promise.all so a multi-input legacy send doesn't serialize the wait.
- BIP86 (Taproot key-path, bc1p…): signInput now uses a tap-tweaked
signer — the internal ECPair, tweaked with sha256("TapTweak" ||
internalPubkey) via ECPair.tweak(). bitcoinjs-lib matches the tweaked
pubkey against the on-chain output key and signs with schnorr. The
input carries tapInternalKey so the PSBT layer knows it's a key-path
spend (no leaf script).
- The plan-time "not yet in this rev" refusal is gone. paymentFor()
returns send: "p2pkh" / "p2tr" for the two families; every path in
the picker signs today.
- Fee vsize model already covered p2pkh (148 vB per input) and p2tr
(58 vB per input) — unchanged.
- Verified in scratchpad/verify-btc-send.mjs: all four families
produce a fully-finalized wire tx (bitcoinjs-lib refuses to
finalize an invalid signature, so a valid extractTransaction()
result is proof the signing path is correct). Vsize per family:
BIP44 222 vB, BIP49 165 vB, BIP84 141 vB, BIP86 142 vB — all
match the input-count/vsize model in this file's fee estimator.
BTC now matches DGB's family selector: pick BIP44 (1…), BIP49 (3…),
BIP84 (bc1q…, default) or BIP86 Taproot (bc1p…) from Settings, on
mainnet or testnet3 (paths shift coin type 0 → 1 automatically).
- lib/chain-btc.js: paymentFor(purpose, node, network) returns the
right bitcoinjs-lib payment (p2pkh / p2sh(p2wpkh) / p2wpkh / p2tr)
keyed off the derivation path's purpose. WalletKeys.entry captures
the family, redeem script (BIP49) and internal x-only pubkey
(BIP86) alongside the standard script/address fields.
bitcoinjs.initEccLib(ecc) is called once at load so p2tr resolves.
- Registry: BTC + DGB address families are purpose-only now; a
helper (addressFamiliesFor / defaultAccountPathFor) computes the
concrete m/PURPOSE'/COIN'/0' per (chain, network) — coin type
{mainnet:0, testnet:1} for BTC, always 20 for DGB. chainMeta
expands the list so the panel doesn't need per-chain knowledge.
- Panel: #btcSettings block mirrors #dgbSettings (family select →
path input auto-fill → Apply). The family-select listener + the
fillFamilyPicker() helper are shared between DGB and BTC — the
DOM prefix is the only per-chain input.
- Send is wired for BIP84 (default) and BIP49 (adds redeemScript to
the PSBT input). BIP44 (needs nonWitnessUtxo prev-tx fetch) and
BIP86 (needs tap-tweaked signer) throw a clear "not yet in this
rev — sweep to BIP84" error so users hit it at plan time, not at
broadcast time. Receive works on all four families today.
- Verified all four families derive the canonical BIP44/49/84/86
spec test vectors for the standard abandon×11 mnemonic — see
scratchpad/verify-btc-families.mjs. Byte-identical to the BIPs.
Seven coins across twelve networks now — BTC joins the shipping roster.
- lib/chain-btc.js: BIP84 native SegWit — m/84'/0'/0'/0/x → bc1q…
(mainnet), m/84'/1'/0'/0/x → tb1q… (testnet3). Reuses the exact same
stack the DGB adapter already pulls in: bitcoinjs-lib for network
params + payments.p2wpkh + PSBT, bip32 for HD derivation, ecpair for
the Signer interface, ecc (@bitcoinerlab/secp256k1) for message-sign
recoverable sigs. No new npm deps.
- Backend: same lib/electrum.js Aegis uses for BCH and DGB — plugged
into a public Bitcoin ElectrumX pool (blockstream.info, lu.ke,
grey.pw) for mainnet and aranguren.org / blockstream.info:993 for
testnet3. Send flow: PSBT build + per-input signInput +
finalizeAllInputs + broadcast. BIP-137 recoverable message signing.
- Registered as btc:mainnet + btc:testnet in COINS with the orange
Bitcoin disc SVG logo. Mount case mirrors DGB (accountPath honored,
so switching to m/44'/0'/0' or m/49'/0'/0' via the setAccountPath
message gives legacy 1… or wrapped-segwit 3… — same one-line UI plumb
as the DGB address-family selector, deferred to a follow-up).
- Panel: sat as the small-unit label, bitcoin: BIP21 QR payload,
chain-aware send placeholder ("bc1q…" mainnet / "tb1q…" testnet).
- Verified: BIP84 spec test vector — abandon×11 mnemonic derives
bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu at m/84'/0'/0'/0/0
(byte-identical to the vector in the BIP text). Testnet variant
produces tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl at m/84'/1'/0'/0/0
(cross-checkable on iancoleman.io/bip39 with coin BTC Testnet).
seedBundledAddons() only copied a bundled add-on when the target folder
was missing, so an updated bundled add-on never landed on any machine
that had ever run Theseus before — the 0.3.14 shipped screenshot editor
would sit in resources/ and be ignored by every dev machine with an
older screenshot/ folder from a previous test.
Compare the bundled addon.json version to the user's on-disk version.
On mismatch, rename the user copy to
<userData>/addons-backups/<id>-<oldver>-<stamp>/ and cp the fresh
bundle in. Backups live outside addonsDir so AddonHost's folder scan
doesn't pick them up as duplicate add-ons under the same manifest id.
Bump screenshot 0.2.0 -> 0.2.1 so the first build carrying this fix
actually reseeds the shipped-0.3.14 editor on existing dev copies.
Users who genuinely fork a bundled add-on should bump their local
version to something different from the bundled one — that keeps them
pinned. Users who edit files without bumping accept upstream updates,
with the timestamped backup as safety net.
Multi-currency coverage matches what aegis.x has been advertising: BCH,
TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for
each. Panel logos, favicon and fallback all read as Aegis.
- Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0
→ secp256k1 → EIP-55 checksummed hex address (verified against
MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC
backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet
override). EIP-1559 send with an inline RLP encoder + secp256k1
recoverable sign; broadcast via eth_sendRawTransaction. personal_sign
message signing follows the \x19Ethereum Signed Message:\n prefix.
- Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519
derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble
ed25519). SLIP-0010 layer verified against spec Test Vector 1 in
scratchpad/verify-slip10.mjs. Native SOL transfer via the system
program with compact-u16 message serialization + ed25519 sign +
sendTransaction. Devnet gets a faucet.solana.com link in Receive; the
panel appends ?cluster=devnet when opening the explorer.
- DGB address family selector (lib/chain-dgb.js already carried the
paths): the Settings block now shows a Native SegWit / Taproot /
Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills
the derivation-path input with that family's default; Apply
rebuilds the wallet against the new path. Address families exposed
via chainMeta.addressFamilies so the panel can render them from data.
- Panel branding: inline SVG shield (hexagonal aspis, same silhouette
as the aegis.x hero) replaces the "?" fallback in logoSvg() and is
what the header shows before a wallet is selected. Data-URI favicon
wired into panel.html so the Theseus sidebar tab icon reads as Aegis
rather than a chain-specific coin mark.
- QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB,
EIP-681 for ETH, Solana Pay for SOL) so external scanners route the
scan to the right wallet.
Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter
protocol (window.solana). The signing paths exist; only the page-inject
bridge glue is missing. History for ETH/SOL is also empty in this rev —
both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress
+ getTransaction pagination for SOL).
Aegis now shares its DGB code with the standalone DigiByte web-wallet at
D:\Dev\SilentCode\Digibyte. Address derivation and PSBT construction come
from that project's @dgb-wallet/core and @dgb-wallet/psbt packages instead
of Aegis-local reimplementations. Any bugfix upstream flows in via a
re-vendor of dist/*.
- lib/dgb/{core,psbt}/ — vendored dist/ output of the two packages plus
a tiny package.json shim marking them as ESM. @dgb-wallet/core's own
import specifier "@dgb-wallet/core" inside psbt/*.js is rewritten to
"../core/index.js" so the sibling module resolves without a workspace.
- New Theseus deps: bitcoinjs-lib, bip32, bip39, @bitcoinerlab/secp256k1,
ecpair — the peer deps the vendored packages need. Loaded via
api.require in index.js's loadDeps().
- chain-dgb.js is a thin adapter now: BIP32 tree via bip32 + DGB
Network object, addresses via core.p2wpkhAddress, tx via
psbt.buildPsbt + PSBT.signInput (per-input, since each UTXO's key
differs) + psbt.finalizeAndExtract. Runtime backend stays the same —
Theseus's lib/electrum.js against the DGB ElectrumX pool.
- Verified end-to-end in scratchpad: abandon×11 mnemonic derives
dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8 (matches iancoleman.io/bip39
and the previous inline implementation, so no on-chain address change
for anyone who was already using Aegis's DGB slot). PSBT build+sign+
finalize on a mock UTXO produces a valid 223-byte witness tx.
BIP44 (D…) and BIP49 (S…) address families are implemented in the
vendored core but not yet exposed in Aegis's picker — the panel needs
an "address family" selector inside the DGB settings block first. Left
for a follow-up; today's DGB pick uses BIP84 native SegWit only.