A seed import offered exactly one prefilled path and no alternatives, so a
seed from a wallet on a different path derived a valid but empty address and
reported a 0 balance. Nothing distinguished "wrong path" from "empty wallet",
which is why repeated imports of a funded wallet all looked identical.
Both real cases hit it. Bitcoin.com derives mainnet BCH from coin type 0' —
its Copay lineage predates the 145' split — while Aegis prefilled
m/44'/145'/0'/0/0. And chipnet tooling derives from 145', while Aegis
prefilled BIP44's testnet 1'.
- A preset dropdown per coin and network, each entry naming the wallets that
path belongs to, with the free-text field kept for anything unlisted.
- "Check which path has my funds" derives each candidate and asks Electrum
for balance and history, five addresses deep per candidate so a wallet
whose first address is spent clean is still found. Results are listed with
balances and a one-click Use; when nothing matches, it says so instead of
implying the wallet is empty.
- The chipnet IMPORT prefill becomes 145'. The vault-derived chipnet account
path stays m/44'/1'/0' — changing that would move the addresses of wallets
that already exist.
The scan derives, queries and returns counts; it stores nothing, and the
mnemonic crosses the same boundary importWallet already uses.
Reported as "WizardConnect pairing is done only via Mainnet, chipnet is not
available on UI". Chipnet pairing works and is not filtered anywhere: the
Connect pane reads state.wallets unfiltered, wcPairableWallets() keys only on
chain/phase/eligibility, and startForWallet registers every BCH wallet
whatever its network. The profile even holds a live wc:// URI against
bch-chipnet-0, so a chipnet wallet has already paired.
What it actually was: the picker listed wallets in registry order, and a bulk
keystore import put fifteen unpairable chipnet WIF wallets ahead of the two
chipnet HD wallets that pair fine. Every visible row was a greyed-out
"can't pair" testnet entry, so the list read as "chipnet is unsupported"
while the working options sat at positions 17 and 19 of 19.
Group the select instead: "Can pair" first, "Cannot pair" after. The reasons
stay visible — they were asked for, and a wallet greyed out with no
explanation reads as broken — but they no longer hide the wallets that work.
Only a WIF import is ineligible, because hdwalletv1 hands the dapp an xpub
and one key is not a key tree. Promote to HD (0.11.0) remains the fix for
those.
Getting a key back out of Aegis only worked for wallets it derived itself.
Every imported adapter's recovery() returned xprv:null with "Recovery lives
in the source of the import", so the wallets most likely to need exporting
were the ones that refused, and the vault-derived ones handed over an xprv
behind nothing but an approval click.
There is one gate now, and it is enforced in the host. revealSecret takes the
master password and verifies it with vault.lifecycle.unlock before it reads
anything; the panel obtains that password either by decrypting the PIN blob,
which wraps exactly it, or by asking. Both routes end at the same proof, so
the host never takes the panel's word for authorisation. The old
recovery({reveal:true}) path is gone and all six Settings buttons route here.
Three wrong PINs switch to the master password rather than dead-ending, and
those attempts still count toward the existing 15-minute lockout, so a
fumbled PIN costs nothing and a guessed one gains nothing. Being locked out
of the PIN also falls through to the password: the lockout exists to stop PIN
guessing, not to lock an owner out of their own key.
"Use PIN to show secret keys" defaults ON, unlike the send flag — a send is
already fronted by an approval overlay, whereas a revealed key is
irreversible the moment it is on screen. Turning it off moves the prompt to
the master password. There is deliberately no setting that reveals a key
without asking for anything.
It is NOT called a recovery phrase, because Aegis has none to show. An import
stores mnemonicToSeedHex(words) and discards the words, vault wallets are
HKDF(vault root, purpose) and never had words, and password-vault.js is
explicit that the seed is never persisted. So each form names itself — WIF,
private key (hex), wallet seed (hex), wallet key (hex) — and says where it
can actually be restored. Someone who writes down what this shows believing
it is twelve words has backed up nothing, which is the one outcome this
screen has to prevent.
A WizardConnect pairing could land on an address the user had never seen.
Pairing took the selected wallet when it qualified and otherwise the first
pairable one in list order, so with the selected wallet ineligible (a WIF
import has no xpub) it silently fell through to whichever wallet happened to
be first. The approval named that wallet by label only, which does not help
when the label is one Aegis generated.
Three changes, one idea: the wallet a purpose uses should be something you
said, not something that fell out of list order.
- Roles. A wallet can be nominated for payments and/or for WizardConnect,
set from its manage modal and badged on its row. A role that points at a
removed wallet reads back as null instead of being trusted, so a stale
pointer can never quietly redirect a payment.
- The pairing approval asks. With more than one candidate it offers a
dropdown of them, each labelled with its own short address; with only one
it shows that wallet's full address. The rows are static, so naming a
wallet above a select the user can change would contradict itself — hence
one or the other, never both. The panel's own Connect pane now follows the
same precedence, because two rules for "which wallet" is how a pairing
surprises someone.
- Send gets a From row listing the wallets on this coin and network, with
balances. It switches the panel selection rather than carrying a separate
source: planSend and send resolve the wallet host-side from that, and a
second notion of "current" would let the form and the approval disagree.
Payments also becomes the opening selection when nothing has been picked yet,
which is what nominating it is for.
No Theseus release needed — approvalModal has supported a select row all
along, and the pick comes back as "allow+wallet=<id>", validated against the
options offered.
Went looking for a performance fix and found a correctness bug underneath it.
getTx() slims each transaction on the way into the cache, and the slim shape
kept only { value, scriptHex } — dropping vout.tokenData. That field is where
the server reports CashTokens. It is NOT in scriptPubKey.hex, which Fulcrum
returns with the token prefix already stripped. So the classify pass fetched
one transaction per UTXO, looked for a prefix that was never there, and
concluded "no token" every single time.
Measured against a real chipnet wallet (130 UTXOs, 91 of them token-bearing):
the old pass cost 54 requests for that address and found 0 tokens; sampling
12 of those transactions, exactly 0 had a scriptPubKey starting with the
token prefix. On the faucet-fed address with 28,289 UTXOs it was ~28k
requests, still finding nothing — which is what made the wallet look hung.
So HD BCH wallets have never shown CashTokens. 0.15.0 fixed the imported
adapter, which reads token_data off listunspent, and I took the HD path's
silence for an empty wallet.
Now: when the server negotiated protocol >= 1.5, listunspent carries
token_data and a UTXO WITHOUT it is definitively not token-bearing, so the
whole set is classified from the one call we already make — 1 request instead
of 28,289. Below 1.5 the fallback fetches parents as before but reads
vout.tokenData (present even at 1.4) and only decodes a prefix as a last
resort, so it is correct now too.
Both routes are reconciled onto one shape. They speak different dialects:
the decoder yields a numeric capability (0/1/2) labelled
immutable/mutable/minting, while Electrum sends a string and calls 0 "none".
Left alone, an identical UTXO would have described itself differently
depending on which server answered. The decoder's vocabulary wins, and the
Certificates pane treats immutable as the quiet default so only capabilities
that change what the holder can do get a tag.
txCache is versioned and dropped once: entries written by the old shape carry
no token information, and an absent field cannot be told apart from "no
token", so a warm cache on a pre-1.5 server would have reported a token
wallet as empty.
Verified against the live wallet: one request, 91 token UTXOs, 46 categories,
17 assets, 51 certificates — matching what the panel reports — with
capability labels normalised (5 immutable, 28 mutable, 18 minting).
Assets was one list holding two different kinds of thing. A fungible balance
answers "how much do I have"; a non-fungible one answers "which ones do I
hold", and they want different rows — an amount against a symbol, versus a
commitment and a capability. On a real chipnet wallet that meant 46 rows
where 32 of them were only ever going to say "1 NFT".
Split by what each category actually holds, so a category carrying both
appears in both — which is honest, because it really does hold both. Assets
counts categories; Certificates counts individual certificates, since "3"
should mean three things you hold rather than three groups.
Certificate rows carry the commitment, because it is the only thing that
distinguishes two certificates of the same category, and a MINTING or MUTABLE
tag, because "can still issue others" versus "is fixed" is worth seeing
without opening anything. A `none` capability is left unlabelled rather than
adding noise to every row.
"Certificates", not "NFTs": these are membership, licence and record tokens,
and NFT carries collectible-market baggage that misdescribes them. Ids and
data keys stay `nft` — that is the CashTokens protocol field, so it is
protocol name inside and product name on screen.
Two things caught while building it. `draw({})` was overwriting the "no
assets" empty state with an empty string, which is now the common case since
purely-non-fungible categories no longer appear there. And certificate rows
printed the category twice, as the title and again on the right — the right
column now only carries it when the row has a real name to lead with.
Metadata is looked up for every category, not just the fungible ones, so
naming a token also names its certificates.
The Receive tab carried two stacked toggle rows: Receive/Consolidate above
Address & QR/Assets. That is a lot of chrome for a 380px panel, and the two
rows were not the same kind of thing — Address and Assets are views of the
wallet, while consolidating is an action on its UTXO set. Consolidate now
sits with the other address actions, beside "Next unused address", with the
sibling count on the button.
The old toggle was also the only way out of the consolidate view, so that
view gains its own "← Back to address". Entering still resets the inline host
so the preview is costed fresh, which is what the toggle did.
The Send tab keeps its Send/Consolidate toggle: there, consolidating really
is an alternative way to send, so a toggle is the right shape.
Removed the two now-dead [data-rcv-mode] blocks rather than leaving selectors
that match nothing.
Verified by real visibility rather than the hidden attribute — a child of a
hidden parent keeps hidden=false, which made a first check look like both
panes were showing at once. rcvNormal and the address pane go away while the
consolidate pane shows, and Back restores them.
Assets listed but every row read as a hex string. Three separate reasons.
**Both default registries were dead.** Checked 2026-09-29:
raw.githubusercontent.com/cashonize/registry/main/bcmr.json returns 404, and
bcmr.salemkode.com does not resolve. So no token resolved a name on any
chain, mainnet included — and the panel's `.catch(() => {})` meant the
failure was completely silent, indistinguishable from a token nobody has
registered. Replaced with OpenTokenRegistry, which answers and carries
chipnet identities. A dead registry is now reported instead of swallowed:
the reply says whether any registry answered, and the hint says so.
**The tokens in question publish no metadata at all.** Not a wallet problem
and not fixable by any registry list: their genesis transactions carry no
OP_RETURN whatsoever, so there is no BCMR authchain to follow and no
registry entry to find. Their names exist only inside the app that minted
them. So a token can now be named locally, per category, stored under
bcmr/local/<cat> and taking precedence over any registry — with a YOURS tag
so a self-assigned name is never mistaken for a published one. Clearing both
fields removes it and lets a registry entry show through again. Optional
decimals, because a raw fungible amount with no scale is its own kind of
wrong: 15000 became "150 GMX" once told there were two.
**The unnamed row printed the category twice**, once as the name fallback and
once as the sub-line. Unnamed rows now show the category as the identity and
"unnamed token · N UTXOs" beneath it.
The header comment also promised a bundled static registry fallback for
well-known tokens. There is no such directory and no load path for one; the
comment is gone rather than left to mislead.
Verified against the real module: local names beat registry entries, a
category with only a local name resolves instead of returning null, clearing
restores the registry value, out-of-range decimals are dropped, and the new
default registry resolves a real chipnet identity (OTRC, 6 decimals). In the
panel: naming a token repaints it with the tag and rescales the amount, and a
non-numeric decimals entry is refused with the modal left open.
Two changes to the same idea: one way to do each thing, and the same shape
for every coin.
**Assets is no longer buried under the QR.** The Receive body stacked
address → QR → assets in one column, so in a 380px panel the token list sat
permanently below the fold behind 200px of QR — and a coin holding no tokens
simply lost a section, which made every coin look structurally different.
Address & QR and Assets are now peers behind a persistent chip row. The count
rides on the chip, and a wallet with nothing says "No assets held by this
wallet" instead of the section vanishing. That mattered more than it sounds:
a chipnet wallet with 46 CashToken categories was indistinguishable from an
empty one until 0.15.0, because the only signal was an absent card.
The choice persists across re-renders and wallet switches — comparing token
balances between wallets should not throw you back to the QR on every click.
Switching back to Address redraws the QR, since a canvas sized while hidden
comes out blank and renderReceive only repaints when the address changes.
**The coin row's ▾ network dropdown is gone.** The coin view already carries
real network chips (data-browse-net: they filter the wallet list and show a
per-network count), so the popover was a second control for one job — and the
only one that hid its options behind a click. Chips are the single network
control now and the ticker is plain text again.
Verified in the panel: chips switch panes and track their own state, the
count shows, the empty state reads correctly, the QR survives the round trip,
and no .wchev / .wcname.wswitchable remain in the DOM.
A real export read "1|buffalo body coyote poem …" and was rejected: the
classifier only accepted a bare phrase, so a version marker in front of the
words was enough to make a valid seed look like junk.
Wallets wrap the phrase in their own envelope — a version number, sometimes
a derivation path, pipe- or comma-separated, sometimes JSON. Rather than
guess which wallet produced it, the payload is now split on every run of
non-letters (spaces survive, since they separate the words) and any
BIP39-shaped run in the pieces is taken as the phrase. A derivation path
found anywhere in the original string is carried over too.
Being tolerant of the wrapper does not loosen what counts as a phrase: the
pieces must still be 12/15/18/21/24 words of 3-8 lowercase letters, so a URL
or an address breaks into single-word pieces and matches nothing. Verified
that the phishing-URL and address cases still fill no field.
The path only lands in the box when the box is empty. That field is
prefilled with the coin's default and may have been edited, and silently
changing which addresses get derived is what lost funds look like; if a path
is already there and differs, the message names both and leaves the choice
to the user. The unrecognised-payload preview also grew to 90 characters,
since 48 truncated the evidence needed to tell what a rejected QR actually
contained.
Import accepts a picture of a QR code. The file is decoded in the panel by
the vendored jsQR, drawn to a canvas: no upload, no network, no camera.
Image, not camera, on purpose. Theseus sets blockCamera + hideMediaDevices
by default and hides device labels from fingerprinting; a camera scanner
would fail silently for everyone until they turned that off globally, and a
wallet should not be the reason a privacy browser gives up the camera. A
photo or screenshot of the code needs no permission at all.
What comes back is classified, never trusted. A QR is opaque to the person
holding it, and "scan this to restore your wallet" is a working phish, so
the decode only chooses which field to fill:
BIP39-shaped (12/15/18/21/24 lowercase words) -> mnemonic field
WIF or 32-byte hex -> private key field
anything else -> nothing is filled; the
decoded text is shown so
the user can see it was a
URL, an address, or junk
Nothing auto-submits. The user reads what landed in the box and presses
Import, and the host handler still does the real validation.
jsQR 1.4.0 is vendored at lib/jsqr.js under Apache-2.0 with its LICENSE
beside it, unmodified and unminified — code that touches seed phrases should
be auditable in the shipped add-on, not an opaque blob. It is 57 KB gzipped.
Verified by round-tripping through the shipped path: Aegis's own encoder
builds the QR, it is rasterised to a real PNG File, and decodeQrFile() reads
it back byte-exact; an image with no code returns null rather than throwing;
and driving the actual file input fills the mnemonic for a seed, fills the
key field for a WIF, and leaves every field untouched for a phishing URL.
The 380px panel is the wrong shape for anything that needs room. This opens
the wallet as a full Theseus tab, with the sidebar's own furniture — identity,
network, balance, section nav, wallet list — laid out as a left rail and the
tab body given to the selected section.
It is the SAME panel.html, loaded with ?surface=web. No second wallet, no
second copy of 4,600 lines to drift apart. That works because an add-on's own
tab is handed a window.silentmode with the same invoke/on surface as the
sidebar, and addon-msg dispatches it as from:"panel" with the add-on identity
derived from the file:// sender — so every existing handler, including the
panel-only ones, works there untouched. The whole change is a CSS grid behind
one attribute plus a chip to open it.
Details that needed care:
- The QR is drag-sized against a 380px panel and the size is remembered.
Given a 720px column it filled the page, so it is capped on this surface
only; the stored sidebar preference is left exactly as the user set it.
- #drop (the coin picker sheet) is fixed-position and sized for the panel;
it is pinned to the rail instead of covering the window.
- Content columns are capped at 720px so forms and lists keep the measure
the sidebar already tuned, rather than stretching across a monitor.
- Under 900px the grid falls back to the stacked layout, so a narrow window
degrades to what the sidebar already does.
- The "open full screen" chip hides itself on the full-screen surface, so it
cannot open a second copy of itself.
Everything is scoped to [data-surface="web"], so the sidebar is byte-for-byte
unchanged. Verified both surfaces, the narrow fallback (by exercising the
real media rule) and zero horizontal overflow.
The aegis.x/app URL still needs a main.js route in Theseus; this ships the
destination over the add-on channel first.
A wallet imported from a single private key cannot do WizardConnect, and no
amount of work inside Aegis changes that: the handshake ships BIP32 xpubs so
the dapp derives addresses without further round-trips, and a lone key has no
chain code to build one from. Manufacturing a parent whose child equals a
given key means inverting HMAC-SHA512, and even solved for index 0 the dapp's
next index lands elsewhere. The way out is to stop being a single-key wallet.
Promote derives a fresh wallet from the vault on the import's own network and
sweeps the key into it, after which WizardConnect works — and so does every
other thing that assumes a key tree. It reuses plan() + signAndBroadcast(),
the same pair the consolidate flow already spends through, rather than
growing a second money path.
Three things it deliberately does not do:
- The preview costs the sweep by planning a send-max to the wallet's OWN
address, so cancelling leaves nothing behind. Same inputs, same single
P2PKH output, so the fee is identical to the real sweep.
- The imported key is kept, not deleted. The sweep is unconfirmed when the
call returns and anyone holding the old address can still pay into it;
removing the key there would strand those coins. Removal stays a separate
step the user takes once the balance reads zero.
- A promote that fails in plan() takes the just-created wallet back out,
since nothing was broadcast. Past that point the wallet is kept even on
error, because a transaction may already be on the wire and its
destination has to stay visible.
BCH only: the BTC/DGB/ETH/TRX/SOL imported adapters still throw "read-only"
from plan(), and the refusal now names the chain instead of failing vaguely.
Wallet creation is lifted out of the addWallet handler into
createVaultWallet so promote builds its destination through exactly the same
purpose allocation, legacy-purpose carry-over and id numbering as the Add
flow, instead of a near-copy sitting next to a transfer.
Eight decimals of tail sat exactly where the eye lands. The hero now shows
two decimals once there is a whole part, because the whole units already
carry the significant digits, and digs past leading zeros otherwise so
amounts under 1 keep roughly four significant figures:
1204.23234145 -> 1,204.23 0.23234145 -> 0.2323
0.00012345 -> 0.0001234 0.00000001 -> 0.00000001
A flat "two decimal places" rule is the obvious version of this and it is
wrong: it renders 0.00042 BCH as 0.00 and the wallet reads as empty. The
rule here never collapses a non-zero balance to zeros, and it truncates
rather than rounds, so the figure shown is never more than the wallet holds
and sending the displayed amount always clears. An ellipsis marks the cut,
hover gives the exact figure, clicking pins full precision and is
remembered. Only this hero abbreviates — Send, MAX, history and the wallet
strip still call fmtBig, so every number acted on is exact.
Also repairs the regexes in the same function, which lost their backslashes
when the earlier commit was scripted: /^(-?)(\d+)(\.\d+)?$/ had become
/^(-?)(d+)(.d+)?$/, matching the letter "d". Still valid JS, so it parsed
cleanly and every balance quietly fell through to unformatted text — the
grouping and the dimmed decimals introduced in bb639a9 had never once run.
Caught by testing the function body extracted from the file rather than a
transcription of it.
First pass of the wallet-app restyle. The panel led with a 22px balance that
carried no more weight than the labels around it, so the one number people
open the sidebar to read had to be hunted for. It is now the hero: 32px,
thousands-grouped, with the fractional part dimmed so magnitude reads before
precision. The ticker and the fiat conversion sit on its baseline as
annotations rather than peers, and wrap to their own line intact once the
amount outgrows a narrow panel.
The Receive/Send/History/Settings bar used an underline for the active tab,
which read as browser chrome and all but vanished at sidebar widths — a 2px
rule under a short label is easy to miss. It is a segmented pill bar now,
active section filled in the accent. The hover tint derives from --ink so it
shows in the light theme too, where a white overlay was invisible.
No charts yet: there is no price history behind Coin-Spectrum to draw, so
nothing here fakes a trend line.
Every chipnet wallet in the picker read "can't pair" with the reason
nowhere on screen: the explanatory note only rendered when NOTHING could
pair, so one working mainnet wallet hid it entirely. The cause now rides
on the row itself ("can't pair (WIF import)") and the note appears
whenever any wallet is blocked. A single private key has no chain code,
so there is no xpub for the handshake to send — the text now says that
and points at the two ways out.
Seed-imported wallets stored the full leaf path (m/44'/1'/0'/0/0) in
accountPath, because that is what derived the one address the strip
shows. WizardConnect was handed that as the BIP44 *account* node and
would derive m/44'/1'/0'/0/0/<branch>/<i>: a tree the user holds no keys
in. Pairing looked healthy and every sign request failed with "no path
for input". Same class as the 0.9.7 chipnet mismatch, one level down.
A dapp drops its pairing code from the DOM once connected, so the
commonest empty scan is a page that is already paired. Sending that user
to "open the Connect dialog" points at a dialog the dapp will not show
again; the message now names the existing pairing instead.
Each .warow was its own grid container, so the `auto` amount column
resolved independently per row: a row holding 0.3066756 got a narrower
column than one holding 0.43789841, and the copy button therefore landed
at a different x on every line. Column widths have to be SHARED to line
up, and nothing was sharing them.
The columns are now declared once on a .walist wrapper and every row
inherits them via `grid-template-columns: subgrid`. Column gaps moved to
the parent, since a subgrid takes its gutters in the subgridded axis from
the grid it inherits and would have ignored them on the row. A
@supports fallback pins the amount column to a fixed 104px for any host
without subgrid, which keeps the copy column straight there too.
Measured across six rows with four different amount lengths, at 260,
320, 400 and 520px wide: copy left edge, amount right edge, ticker left
edge and menu right edge all have 0px spread, with no row overflow and
no clipped amount.
0.9.8 claimed this area was verified, but the check only looked for cell
overlap WITHIN each row and never compared the same column ACROSS rows —
which is exactly the defect it missed.
The row's grid template still described the pre-0.9.2 layout — six
columns including an address cell that no longer exists — while the row
renders five. Every cell therefore sat one column left of where it
belonged: copy landed in the label's space, the amount in the old label
column, and the ⋯ in the amount column instead of the edge. That is the
whole cause of "the copy button collides with the amount" and "the last
edit button is not on the edge".
Now five columns for five cells: icon · label · copy · amount · menu,
8px gaps. The label is the flexible one, left-aligned, so it takes the
slack and truncates rather than squeezing the number. Verified at 520,
400 and 300px: no cell overlap at any width, no row overflow, and the
amount never clips — only long labels give way.
The copy glyph was the 📋 emoji, which has no glyph in this platform's
font stack and rendered as a tofu box that read like a stray character
stuck to the balance. Replaced with an inline SVG in both the row and
the address card. Both confirmation flashes now swap innerHTML rather
than textContent, which would have deleted the SVG and left a blank
square.
Address card: the address and its copy button share one row, so the
button sits at the end of the value it copies. The address clamps to two
lines and truncates beyond that instead of growing the card in a narrow
sidebar.
The QR is always visible and the panel is drag-resizable from a grip
under it (pointer events, arrow keys as a non-mouse path, clamped
90–420px, capped against the panel's own width so a size set on a wide
sidebar cannot overflow a narrow one, persisted). That replaces the
0.9.2 show/hide toggle — a size set once beats a binary, and it frees
the row Copy was sharing with a QR button.
drawQr was setting cv.style.width/height, which would have reset the
panel to its intrinsic size on every redraw — i.e. every time the
address changed. It now sets only the backing store and re-asserts the
user's size after drawing.
"Next unused address" is correct at the adapter level on both mainnet
and chipnet (tested: the index advances and the address changes), so the
reported failure is elsewhere. The handler was discarding the error and
flashing a bare "Failed", which is why there was nothing to diagnose; it
now surfaces the real message.
"+ Add another BCH" created a fresh wallet on the spot. The old comment
argued that being inside a coin's address list made the intent
unambiguous; it isn't. "Add another BCH wallet" is just as often "bring
in the one I already have somewhere else", and guessing wrong is not
harmless — the user gets an empty new address and has to work out for
themselves why their funds aren't in it.
Adds aegisChoose(), a pick-one sibling of aegisConfirm for branches
where the honest answer is a question rather than a yes/no, and puts it
in front of every path that reached addWallet:
- + Add another <TICKER> in the coin drilldown
- + Add on an unowned coin in the browse picker (now routes to the
existing New / Import / Connect chooser, with the coin still
preselected through whichever branch is taken)
- + Add your first wallet on the empty state — the most important one,
since someone arriving with an existing seed was being handed a
create-only flow
The empty-state copy claimed "there's no separate seed to import",
which stopped being true when imports shipped and actively told users
the feature they wanted did not exist.
Verified in a rendered panel: the chooser appears, addWallet is not
called until Create is picked, Import opens the import modal, and
Cancel does nothing.
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.
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.
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.
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".
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.
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.
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.
2026-09-08 18:17:25 +02:00
Renamed from bundled-addons/bchwallet/panel.js (Browse further)