Commit graph

36 commits

Author SHA1 Message Date
Local Dev
4511a933f4 Aegis: a PIN clears only the transaction it was entered for
One global 90 s clearance was opened by every PIN proof. Opening the
wallet or ticking a setting let the next dapp transaction from any site
through without a PIN; a dapp waiting in its poll could take the clearance
the user had just made for their own send; and with two sites waiting the
second one's request was dropped.

Each waiting dapp transaction now has an id, and only a proof naming that
id releases it; a proof given for "transaction" in the panel clears the
panel's next send only; any other proof clears nothing. The panel answers
waiting sites one at a time. Promote to HD now asks for the PIN like any
other spend instead of failing when PIN-per-transaction is on.
2026-10-04 02:23:02 +02:00
Local Dev
2faa3d808a Aegis: a WizardConnect request the overlay cannot read is not signed
wc-sign accepted a flat request (tx and sourceOutputs at the top) that
buildWcApproval does not read, so any paired dapp could get a signature
after an overlay showing only the wallet, an input count and the sighash.
The signer now takes only the nested WizardConnect shape, and the overlay
refuses, without showing anything, a request whose outputs or spent
inputs it cannot decode. Total out now sums every output, not the first 8.
2026-10-04 02:21:11 +02:00
Local Dev
e02d4698ea Aegis: a permit is described from what the signature covers
describePermit read td.message directly, and the EIP-712 encoder ignores
keys a type does not declare. A dapp could put a decoy allowed:false or
details:{amount:"1"} beside an unlimited permit and the overlay showed
the decoy, not risky, under the plain Sign button, with the same digest.

The overlay and the message preview are now built from the declared
fields only, the permit variant is picked from the declared type, and
dropped fields are counted on the overlay. Amounts of 2^96 units and up
are flagged as effectively unlimited, marketplace orders and Safe
transactions get the warning too, and the encoder refuses a non-hex
address instead of signing it as zero bytes.
2026-10-04 02:19:42 +02:00
Local Dev
b85605416e Aegis: the PIN is checked by the host, and guessing ends at five
The panel fetched the PIN blob and decrypted it itself, then reported its
own failures. The lockout therefore counted only what a well-behaved panel
chose to report, a 15-minute timer handed out five more guesses forever,
and anything able to run in the panel could take the blob and search the
million PINs offline in minutes.

The blob now never leaves index.js: pinSet builds it after checking the
master password against the vault, pinUnwrap counts each guess before
trying it, and five wrong guesses switch the PIN off until the master
password is entered. The panel keeps its PIN pads and only sends digits.
A blob from an older build (200k iterations) is re-made at 600k under a
fresh salt on the next correct PIN. Only the topmost PIN pad listens to
typed digits, so two stacked pads cannot both take one entry.
2026-10-04 02:17:26 +02:00
Local Dev
884f3948b8 Aegis: fees that follow the network, connections that come back, its own name on Solana
Still 0.31.0 (unpublished batch).

- ETH nonce. The pending count from a load-balanced RPC often misses a
  transaction this wallet sent seconds ago, so two sends in a row shared a
  nonce and the second failed or replaced the first. The nonce is taken at
  signing, from the RPC or from what the wallet itself last broadcast,
  whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
  transaction; they rejected the type-2 envelope, so a network added by a
  dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
  estimate instead of a constant, size each output from its real script
  (a taproot destination was undercounted), and round the size up before
  pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
  sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
  server dropped. The client reconnects with backoff, pings to catch dead
  sockets, times out a silent connect, and hands the replayed subscription
  answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
  balance.
- Removing a wallet retired its derivation index for good. Add wallet now
  takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
  setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
  page-inject allow-list in step with enabled and connected sites.
2026-10-04 01:55:38 +02:00
Local Dev
f27f3d27c9 Aegis: the PIN is enforced by the host, and every spend is confirmed there
PIN
- The PIN blob wraps the vault master password under six digits and sat in
  plain add-on storage, so a copy of the profile reduced the master password
  to a million offline PBKDF2 guesses. It is sealed with the OS keystore
  before it is stored; a plain blob from an older build is sealed on first
  read. New blobs use 600k iterations.
- "Ask for PIN on every transaction" was decided by the host and enforced
  by nobody: `send` never checked it and dapp transactions had no PIN step.
  A gate is now cleared only by the master password the PIN unwraps,
  verified against the vault, which also opens a single-use transaction
  clearance. Panel sends consume one; dapp transactions ask the open panel
  and wait.

Spending and signing
- Consolidate emptied wallets on the panel's confirmation alone, defaulted
  to every sibling when no list was sent, and swept into whatever was
  selected at click time. It now needs explicit sources and the previewed
  destination, and shows the whole batch on the host overlay.
- Typed data for a chain other than the connected one is refused. Permit
  and Permit2 signatures name the spender, tokens, amounts and expiry, and
  an unlimited one gets the danger action. Previews are no longer cut at
  600/400 characters without saying so.
- eth.rpc relayed any eth_* call for sites that never connected.
- The WizardConnect overlay lists the tokens being spent and received.

Send form
- "0,5" was read as 5: the parser deleted commas. Amounts are parsed
  exactly; a decimal comma is a decimal, ambiguous or non-numeric input is
  refused, extra decimals are an error instead of being dropped.
- Send submits the request the summary was computed for, never a fresh read
  of the form, and stays off while a plan is pending or stale.
- Switching wallet resets the form instead of leaving a live button on the
  previous wallet's plan.
- The unlock field kept the master password after unlocking; the PIN pad
  kept its digits and kept counting keystrokes typed elsewhere as PIN
  attempts; an idle lock left a revealed key or seed form on screen.
- Enter confirmed a dialog even with focus on Cancel.
2026-10-04 01:38:53 +02:00
Local Dev
03140fae9d Aegis: WizardConnect signing requests can be approved
approvalRequest passed approve/reject keys the host overlay does not
know, so it showed a lone "OK" button whose id never equalled
"approve": every WizardConnect signing request was refused, and the
HTML body was shown as literal markup. It now passes a Sign action and
plain rows: wallet, input count, each output decoded to a cashaddr on
the wallet's network (or OP_RETURN / raw script), total, and who
broadcasts.
2026-10-03 23:01:15 +02:00
Local Dev
206f54edd2 Aegis: Tron signing overlay comes from the bytes being signed
The overlay for tronWeb-built transactions was built from the dapp's
raw_data JSON, which need not match raw_data_hex: a site could show
"1 TRX to X" and get a signature over anything. lib/tron-decode.js
decodes Transaction.raw from raw_data_hex (contract type, owner, to,
amount, TRC-20 transfer/approve calldata, fee limit, memo); the txID
must match the bytes, every contract's owner must be this wallet, and
unlimited approvals or permission/resource delegation get a danger
action.

sendRawTransaction relayed any signed transaction a site handed it;
it now broadcasts only txids Aegis itself signed.
2026-10-03 22:53:58 +02:00
Local Dev
0e7d6cc3c4 Aegis: Solana bridge signs the real bytes and shows what they do
- signAndSendTransaction signed String(Uint8Array) ("1,2,3,..."), so
  every dapp transaction got an invalid signature. Adapters gain
  signBytes(), which signs the exact message bytes.
- v0 (VersionedTransaction) messages were parsed with the version byte
  as the header; the shared parser handles legacy and v0.
- window.solana.signTransaction went through signMessage and its
  "moves no SOL" overlay. It now has its own handler and overlay, and
  signMessage refuses bytes that parse as a transaction (Phantom's rule),
  since such a signature is a valid transaction signature.
- Overlays decode System transfers and SPL transfer / approve /
  set-authority, and list everything else as not decoded.
2026-10-03 22:52:41 +02:00
Local Dev
314bce0905 Aegis: EVM bridge signs what the dapp asked for, chain per site
- eth_sendTransaction dropped the calldata, gas and fee fields, so an
  ERC-20 transfer went out as a 0-value send to the token contract and
  any contract call was broadcast as something else. plan() now carries
  data/gas/fee caps/nonce, estimates gas for calls, and the overlay
  decodes transfer/approve/permit/setApprovalForAll, flags unlimited
  approvals and calldata to a non-contract, and shows estimated vs max
  fee.
- personal_sign signed the hex string ethers/viem send as literal text;
  it now signs the decoded bytes.
- wallet_switchEthereumChain flipped the global selected wallet, so any
  site could move every connected dapp to another chain. Chain is now
  per origin; the sidebar selection no longer redirects a dapp.
- wallet_addEthereumChain silently persisted a connection for chains
  Aegis already had, handing the address to any site. Adding a chain
  no longer grants anything, and RPC URLs must be https.
- eth_sign (blind hash signing) is disabled, as in MetaMask.
2026-10-03 22:51:05 +02:00
Local Dev
4c3d27f1b3 Aegis: a plain Connect now lasts for the session
Connecting without ticking "Always allow" stored nothing, so the
site's very next call (personal_sign, signTransaction, ...) failed with
"not connected". Plain Connect now grants the origin in memory until
Theseus restarts; "Always allow" still persists. Every bridge (BCH,
Tron, EVM, Solana) checks grants the same way, revoke clears both, and
the connected-sites list shows EVM/Solana grants and which ones are
session-only.
2026-10-03 22:49:15 +02:00
Local Dev
9f46d932b6 Aegis 0.29.0: start on first use, not at every Theseus launch
Aegis was most of what was left of launch cost: its crypto and
WizardConnect deps block the main thread for ~0.6 s right after the first
frame. It now declares its panel and "activation": "on-demand"; the
dapp bridges are still on every page from the start, and the first page
call, panel open or wiz:// link starts it.

activate() returns a promise the host waits on, so the call that woke
Aegis finds WizardConnect and the mounted wallets. With a locked vault it
does not wait for the mount (derive blocks until unlock), so the page gets
the usual locked answer in ~0.7 s instead of after the host's 5 s limit.

Two things only work while Aegis runs: a live WizardConnect pairing (no one
else listens on its relays) and "stay unlocked" (which also opens
Settings > Passwords). While either is on, Aegis asks the host to start it
at launch, and withdraws the request when both are off. Pairings left
behind by a removed wallet do not count.

Boot trace, same profile, 3 warm runs: main thread blocked in the first
6 s 970-1040 ms -> 300-320 ms, longest block 605-667 ms -> 227-244 ms,
toolbar 1.34-1.61 s -> 0.83-0.87 s.
2026-10-03 21:17:39 +02:00
Local Dev
469ff482ea feat(aegis): recover DigiByte seeds from the 2018-19 mobile wallets
Ported from Digibyte.X/dgb-wallet @ 989a2696.

BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's
official 2018-19 Android/iOS wallets were BreadWallet forks and used
"DigiByte seed" as the HMAC key instead, so the same twelve words produce a
completely different key tree — every address differs, and a standard scan
finds nothing at all. Anyone importing one of those seeds into Aegis got a
valid, empty address and a zero balance with no way to tell why. Same shape
as the Bitcoin.com coin-type-0 problem.

- rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC
  key, matching upstream, and exports HMAC_BITCOIN_SEED /
  HMAC_DIGIBYTE_SEED.
- lib/import-derive.js grows the same option, since that is what actually
  derives the address on an import, and importWallet records the variant on
  the spec so the entry says which tree its stored address came from.
- The path scanner added in 0.24.0 now covers DigiByte: all four purposes
  under the standard key, plus m/0' and BIP44 under the legacy one. So the
  answer to "which tree holds my coins" is a scan rather than a guess, and
  the Use button carries the key along with the path.

Verified against an independent HMAC-SHA512 computation, and end to end: the
same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under
the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn.

Also fixes a coupling this exposed: lib/import-derive.js derives taproot
addresses but never called initEccLib, relying on chain-btc.js (and formerly
lib/dgb/core/address.js, before it went lazy here) doing it at load time. It
now installs the schnorr backend itself, once, so its taproot output no
longer depends on an unrelated module's import order.

The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is
upstream-only for now — Aegis's imported DGB adapter watches a single stored
address rather than scanning a gap.
2026-10-03 14:13:25 +02:00
Local Dev
c1988b9674 fix(aegis): DigiByte import failed on every OTA install
Reported as "the Aegis DigiByte import error". The vendored @dgb-wallet/core
modules under lib/dgb/ imported "bitcoinjs-lib", "bip32", "ecpair",
"bip39" and "@bitcoinerlab/secp256k1" as bare specifiers. Node's ESM resolver
walks up from the importing file, so that resolves in a dev checkout — where
Theseus's node_modules sits above the add-on — and resolves nowhere once
Aegis is running from <userData>/extensions/aegis/, which is every OTA
install. The import threw, index.js caught it and set dgbCore = null, and any
DigiByte import then died in digibyteNetwork() with "DGB adapter not
available".

So DigiByte worked on a fresh install and disappeared after the first update.
Confirmed on this machine: the only node_modules reachable from the installed
extension carries bip39 and none of the other four.

lib/dgb/deps.js now holds the dependencies, injected by index.js from
api.require (which resolves against the app tree) before anything under
lib/dgb/ is imported — the same pattern every other lib/*.js in Aegis already
uses, and the reason that pattern exists. Consumers read them through
accessors rather than capturing them at module scope, so import order is no
longer load-bearing: hd.js builds its bip32 on first use and the two
initEccLib callers go through a once-only ensureEcc(). Using a DGB module
without injection now throws a named error instead of a resolver failure
swallowed into a null.

Verified by loading the modules from a directory with no reachable
node_modules and deriving real addresses: BIP44 D…, BIP49 S… with its legacy
3… pair, BIP84 dgb1q…, BIP86 dgb1p… taproot, plus a WIF round-trip and the
non-DGB WIF rejection.
2026-10-03 13:59:01 +02:00
Local Dev
40683c84cf perf(aegis): stop freezing Theseus — a 7.4 MB store parsed on every read
Reported as "opening Aegis makes Theseus get stuck / not responding", and it
was Aegis's fault.

The host's add-on store is ONE JSON file per add-on, and storage.get() does a
readFileSync plus a JSON.parse of the whole thing on every call —
synchronously, on the Electron main thread, the thread that drives the entire
browser. storage.set() additionally stringifies and writes all of it.

That store had grown to 7.4 MB, 99.9% of it one wallet's txCache: getTx()
kept every transaction it ever fetched, with full vin/vout arrays, and a busy
chipnet test wallet had thousands. Measured on the real file: parse 59 ms,
stringify 39 ms. So one storage.get blocked the UI for ~60 ms, one set for
~99 ms, and fullState() — which reads the store seven times over
selectedWalletId, walletRoles, walletEntries, snapshotForSelected and the
server list — cost ~420 ms. emitState() runs on every adapter change, across
nine wallets, so the main thread was never given back.

Three changes:

- txCache is capped at 400 entries, pruned newest-first by block time
  (unconfirmed entries sort as newest — they are the current ones). Worst
  case ~0.3 MB per wallet instead of unbounded.
- TX_CACHE_VERSION 3, so existing oversized caches are discarded on first
  load rather than needing a manual clear.
- loadHistory() only persists when something actually changed; an idle wallet
  was rewriting the whole file on every poll for nothing.
- api.storage gains a write-through read cache, so repeated gets cost one
  parse per process instead of one per call. Writes still go to the host
  unchanged. Safe because this process is the only writer — Aegis's panel
  talks over addon messages and never touches addon storage; if that changes,
  the cache has to go.
2026-10-03 10:56:59 +02:00
Local Dev
64b75c5bf4 feat(aegis): the BCH bridge works on any site, not just .x
window.bitcoincash was gated on a ".x" hostname in both the preload and the
host handlers — inherited from the pre-multi-wallet build, when every dapp
Aegis knew about was a Silent Mode one. It was never a security boundary: the
TRX / ETH / SOL bridges have always injected everywhere, and every BCH call
still crosses into main, raises an approval overlay and honours per-origin
permissions. The preload cannot read an address or sign anything by itself.

What the gate actually cost was Cauldron, bch.guru and our own dapps on other
TLDs (potidaea.asm) seeing no BCH provider at all, on a wallet whose entire
subject is BCH. WizardConnect was never gated this way, so the two halves of
the same bridge disagreed about who could talk to it.

isBchOrigin becomes isDappOrigin and now only keeps non-web schemes out —
file://, chrome://, data:, javascript: and anything unparseable.

Separately: page-inject declared https://*/* only, so a dapp served from
http://127.0.0.1 got no preload at all, which is a different cause from the
.x gate and would have survived removing it. localhost, 127.0.0.1 and [::1]
are now matched over http as well (the host's compiler makes the port
optional, so :3000 and :5173 are covered). Plain http on any other host stays
out, since an http page is tamperable in transit and only local development
needs the exception.
2026-10-03 09:45:01 +02:00
Local Dev
be05fe67d4 feat(aegis): choose when Aegis asks for the PIN
Reported: after a restart Aegis opens without asking for a PIN, then demands
one as soon as you click something. That came from the only control being
requirePinForSending — safeStorage remembers the master password, so the
wallet reopens unlocked, and the PIN prompt then ambushes the first action.
Asking at the door is coherent. Asking nothing is coherent. Asking once the
user is already inside is not.

"Ask for PIN" is now four independent triggers, because wanting one at
startup and again per transaction is a normal combination:

  - On each browser restart  (compares a per-process boot id; a timestamp
                              cannot tell a restart from a long idle)
  - On each wallet launch     (hide/show — one panel load is one launch)
  - Every 6 hours
  - Each transaction

With none ticked, an open wallet is never interrupted again. The decision is
made host-side: the panel reloads on every hide/show and must not be the
thing that remembers a gate was cleared. A clearance is only recorded after
the panel has actually decrypted the PIN blob, which is proof rather than a
claim, and ticking a trigger does not fire it retroactively.

restart defaults ON for a wallet that otherwise reopens fully unlocked, and
transaction inherits the old requirePinForSending so nobody loses a gate they
had chosen. Removing the PIN clears every trigger and the clearance record.
2026-10-02 20:46:36 +02:00
Local Dev
57177a3439 fix(aegis): refresh was a silent no-op, and reported success either way
Three separate reasons the Refresh button looked dead:

- wallet.js refresh() returned immediately when state.scanning was set, so a
  manual press during a background poll did nothing at all. A forced refresh
  now awaits the in-flight pass and then does real work; background polls
  still yield. scanning is only ever written inside doRefresh, which only
  refresh() calls, so scanning implies a pending inflight to wait on.
- The adapters catch their own fetch failures onto state.error instead of
  rejecting, so awaiting refresh() proved nothing and refreshChain reported
  ok:true for a wallet that had just failed against a dead server. It now
  reads the snapshot back.
- Success changed only a tooltip. Balances that were already current left the
  screen identical, which is indistinguishable from a broken button. It now
  flashes a result and says how many wallets were refreshed, or how many
  failed and why.

Version bumped once for this batch; not published yet.
2026-10-02 20:34:01 +02:00
Local Dev
0b4ddee8c5 feat(aegis): 0.24.0 — ask the chain which derivation path holds the funds
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.
2026-10-02 00:39:54 +02:00
Local Dev
6c5fa9cea8 feat(aegis): 0.22.0 — show a wallet's secret key, behind the PIN
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.
2026-10-02 00:04:06 +02:00
Local Dev
6bb3c50d0d feat(aegis): 0.21.0 — you pick which wallet pays and which one dapps get
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.
2026-10-01 00:59:17 +02:00
Local Dev
0f3dd6e9ca fix(aegis): 0.17.0 — both BCMR registries were dead, and self-minted tokens can now be named
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.
2026-09-29 00:13:30 +02:00
Local Dev
6d844b03cf fix(aegis): 0.14.0 — a seed import is an HD wallet, not one address
Importing a Bitcoin.com seed showed a balance of zero. Two causes, one mine.

Mine: a Copay / Bitcoin.com backup QR is "1|<words>|<network>|<ACCOUNT
path>|…", and 0.13.2 dropped that path straight into a box whose contents are
derived as a LEAF. Deriving the account node itself yields an address the
wallet has never used — for the standard BIP39 vector,
qr96x72dpwrjmg8gtmfemmdhn6u8aqdgnvn4fp2906 instead of
qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6. An address with no history, so:
zero. An account path now extends to /0/0 instead of being derived in place.

The deeper one: a seed import mounted on the single-address adapter, which
watches exactly one scripthash. Bitcoin.com, Electron Cash and the rest
spread funds across a whole BIP44 account, so even with the right leaf the
balance only shows if it all happens to sit on the first receive address.
A seed import is an HD wallet and now mounts as one — the same BchWallet the
vault-derived wallets use, whose WalletKeys walks receive AND change to a gap
limit of 20. That is what actually finds the money, and it brings real spend
support to seed imports as a side effect.

WIF imports are unchanged: one key is one address, nothing to scan.

Existing seed imports are picked up without re-importing. accountPath is
stored in either shape — older imports kept the full leaf, the QR carries the
account — and the mount trims both to the last hardened element before
handing it to WalletKeys. The stale display address stored at import time is
irrelevant, since the panel reads the address off the adapter's snapshot.

Mounting now reads the signer up front to decide which adapter to use. A
locked vault still mounts watch-only from the stored address rather than
failing, and the WC manager gets its own copy of the root because it keeps a
live reference for the per-URI relay-identity HKDF.
2026-09-28 22:29:19 +02:00
Local Dev
b0d70dd942 feat(aegis): 0.12.0 — open the wallet full screen, sidebar becomes the rail
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.
2026-09-27 20:17:08 +02:00
Local Dev
e0246f8520 feat(aegis): 0.11.0 — promote a WIF import to an HD wallet
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.
2026-09-27 16:19:24 +02:00
Local Dev
464d83316b fix(aegis): 0.9.10 — say why a wallet can't pair, and stop imports advertising the wrong key tree
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.
2026-09-27 00:01:39 +02:00
Local Dev
36012e7c26 fix(aegis): 0.9.7 — chipnet WizardConnect advertised the wrong key tree
A chipnet BCH wallet derives from m/44'/1'/0', but the WizardConnect
registration fell back to a hardcoded m/44'/145'/0' whenever the entry
had no explicit accountPath — which is the normal case for a wallet
created through the UI. Mainnet's default happens to be that same
literal, so only chipnet was affected.

The consequence was worse than a failed pairing. Pairing SUCCEEDED, the
handshake carried xpubs for an unrelated key tree, and the dapp then
derived addresses this wallet does not own:

  wallet's real chipnet address : bchtest:qpezx8qkwpjd4e6pd5aang0ve6fctpjvg5ckp2lwu7
  address from the WC xpub      : bchtest:qzvpe3w9rqnszk6mntnef6zv6v94zmfvpc49qkxml4

So the dapp saw an empty stranger's wallet, and anything it built spent
inputs the wallet could not match — signing would fail with "no path for
input". Silent, and only reachable on testnet.

Both registration sites (vault-derived and imported) now take the path
from adapter.snapshot().accountPath, which is by construction the tree
the wallet actually derives its addresses from.

Two wrong turns worth recording. defaultAccountPathFor() takes the coin
CONFIG object, not a chain string, so passing entry.chain returned null
and would have stopped WizardConnect registering at all — strictly worse
than the bug being fixed. chainMeta().defaultAccountPath was no better:
BCH has no coinType in COINS, so it is null for every BCH network. The
adapter is the only component that resolves this correctly, which is why
it is now the source.
2026-09-24 04:56:12 +02:00
Local Dev
7a71b08cf7 fix(aegis): 0.9.1 — an unset custom RPC no longer becomes the text "undefined"
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.
2026-09-23 00:24:42 +02:00
Local Dev
8174fccba0 feat(theseus+aegis): WizardConnect auto-detection — wiz:// links + page scan
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.
2026-09-23 00:16:49 +02:00
Local Dev
1b74b10fc5 chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page
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.
2026-09-22 23:08:16 +02:00
Local Dev
cb7ca53b01 chore(aegis): 0.8.2 — sectioned Settings tab
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).
2026-09-22 20:37:28 +02:00
Local Dev
f46e9112b7 chore(theseus): 0.3.47 — plug-in category + panel-driven addon self-update, aegis 0.6.31
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".
2026-09-14 02:30:51 +02:00
Local Dev
c9a3db26ce fix(theseus/boot): paint the toolbar first — stop gating startup on chrome.html's load event
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.
2026-09-09 11:40:45 +02:00
Local Dev
992c02ea89 feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
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.
2026-09-09 10:33:21 +02:00
Local Dev
344c71b315 fix(theseus/aegis): drop registerSidebarPanel icon override so dock inherits brand shield
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.
2026-09-08 22:40:48 +02:00
Local Dev
2e54bf5e5a Ship Theseus 0.3.28 5d15508b (Aegis update card + DevTools in tab sidebar + real favicons)
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/index.js (Browse further)