diff --git a/GOTCHAS.md b/GOTCHAS.md index aa2724d6..d6a45faa 100644 --- a/GOTCHAS.md +++ b/GOTCHAS.md @@ -81,6 +81,24 @@ The same applies to `home-preload.js`, which is in *every* tab. Handlers behind it check `isHomePageSender` / `isErrorPageSender`, which compare the sender against the exact shipped file:// URL. +## An enabled add-on is not necessarily running + +An add-on whose `addon.json` says `"activation": "on-demand"` is listed at +launch but not started. Its declared `panels`, `toolbar-menu`, +`context-menu-items` and `page-inject` bridge are live anyway, and +`AddonHost.dispatch()` starts it on the first call (see `ensureActive` in +[addons-host.js](addons-host.js)). So in main: + +- Reach an add-on through `addonHost.dispatch(...)`, never by checking + `isActive()` / `hasHandler()` first. Both are false for a waiting + add-on, and the call would never wake it (`routeWizUri` shows the + pattern for when you really need to check). +- "Enabled" in the snapshot means running or waiting; `running` tells + them apart. +- Settings › Performance › "Start extensions when first used" off makes + every add-on start at launch again, which is the quickest way to rule + this out when an add-on "does nothing". + ## The resolver in Theseus is `resolver-web.mjs`, not `.js` Packaged builds ship `Argus/src/lib/resolver-web.js` as `resolver-web.mjs` diff --git a/NOTE-aegis-on-demand.md b/NOTE-aegis-on-demand.md new file mode 100644 index 00000000..674c6208 --- /dev/null +++ b/NOTE-aegis-on-demand.md @@ -0,0 +1,147 @@ +# Hand-off: switching Aegis to on-demand activation + +Theseus can now start an add-on on first use instead of at every launch +(`"activation": "on-demand"` in `addon.json`, see [addons-host.js](addons-host.js)). +The small bundled add-ons use it. **Aegis was deliberately left on startup** +because another session owns `bundled-addons/aegis/*`. It is where most of +the launch win is: measured 2026-10-03, Aegis costs ~165 ms of synchronous +activation plus ~610 ms of main-thread work right after (`loadDeps`: noble +curves, bitcoinjs, libauth, WizardConnect). This note lists what Aegis has to +declare and handle to switch. It was written against Aegis 0.27.1 +(branch `claude/sleepy-maxwell-251ee6-b`). Nothing in the host needs to change. + +## What the host already does for an on-demand add-on + +- While the add-on waits, the host still serves everything it declares: + `panels` (dock button + sidebar), `toolbar-menu`, `context-menu-items` and + the `page-inject` bridge. The bridge source is read from disk the first + time a matching page loads, so `window.bitcoincash`, `window.ethereum`, + `window.solana`, `window.tronWeb`/`tronLink`, `window.wizardconnect` and + the EIP-6963 announcements are all on the page **before** Aegis runs. +- These start it, then deliver the call. A call never gets dropped: it waits + for the activation, and parallel calls share one activation. + - any `addon-page-msg` from the bridge (`getAddress`, `eth.*`, `sol.*`, + `trx.*`, `wcPageReady`, `wcConnectFromPage`, …) + - any `addon-msg` from `panel.html` or an Aegis full-tab page + - opening the Aegis panel (`setSidebar` starts it alongside the page load) + - a `wiz://` link click (`routeWizUri` in main.js wakes Aegis, then + dispatches `wcConnectFromPage`) + - Settings `addon-invoke` calls +- **If `activate()` returns a promise, every call waits for it, up to + 5 s** (`READY_WAIT_MS`). After 5 s the call goes through anyway. +- `api.startAtLaunch(true|false)` lets an add-on ask to be started at the next + launch even though its manifest says on-demand. The host saves the choice + in `settings.addonsStartAtLaunch`. Settings › Performance › "Start the + wallet at launch" does the same thing from the user's side, for `aegis` + only. +- Settings › Performance › Startup already has the "Start the wallet at + launch" switch. It is disabled, with a hint, until the installed Aegis + manifest says `"activation": "on-demand"`. After that it works with no + Settings change (default: off). + +## What Aegis has to change + +### 1. `addon.json` + +```json +"activation": "on-demand", +"panels": [{ "id": "main", "title": "Wallet", "page": "panel.html" }], +``` + +Leave `icon` out of the panel entry so it inherits the manifest icon, as +today. The `api.registerSidebarPanel({ id: "main", … })` call in `activate()` +can stay: the same id replaces the declared entry. Bump the version, or +existing profiles keep the old manifest (`seedBundledAddons` only reseeds a +strictly newer bundle). + +### 2. `activate()` must return a "started" promise, and must not wait for the vault + +Today `activate()` returns `undefined`. It registers every handler +synchronously, then loads deps after `whenUiReady()`, then `tryAutoUnlock`, +then `mountAllWallets()`. That was fine at launch, because dapps arrived +seconds later. On demand, **the call that woke Aegis is the first thing it +sees**. Right now that call would fail: + +- `getAddress` / `signAndSend` / `signMessage` → `legacyBchRuntime()` throws + "wallet is not ready (vault locked?)" because no runtime is mounted yet +- `wcConnectFromPage` throws "WizardConnect is still starting up" +- `wcPageReady` answers `{available:false}`, and the dapp falls back to a QR +- `eth.*` / `sol.*` / `trx.*` see no runtimes + +Fix: return a promise that resolves once Aegis can actually serve calls: + +```js +activate(api) { + …register handlers exactly as today… + const started = uiReady.then(() => loadDeps(api)).then(async (d) => { + …c.d = d; c.wc = …; // as today + await tryAutoUnlock(api); + const st = await api.vault.lifecycle.status().catch(() => null); + const mounting = mountAllWallets(); + if (st && st.unlocked) await mounting; // see below + }); + started.catch(…); // as today + return started; +} +``` + +**Vault-derive waits:** `api.vault.derive()` does not reject while the vault +is locked. It polls until the user unlocks (main.js `vaultDerive`: +`while (!vaultState) await …`). So with a locked vault, +`mountAllWallets()` never resolves. **Never await it in the returned promise +unless the vault is already unlocked.** The host would wait out the full +5 s on every first call, and then deliver the call to a wallet that is +still not ready. With a locked vault, resolve right after deps load. The +existing locked-vault behaviour then applies unchanged: the panel shows its +unlock gate, and page calls get "wallet is not ready (vault locked?)", same +as a locked vault at launch today. + +The ~610 ms `loadDeps` cost now lands on the first dapp call or panel open, +not on launch. `whenUiReady()` has long resolved by then, so it runs at once. +That is the intended trade, but it is visible: the first +`eth_requestAccounts` approval appears about 0.6–1 s later than it does +today. + +### 3. Live WizardConnect pairings must keep Aegis at launch + +`lib/wc.js` saves pairings under `wc//uris` and reconnects them to +the Nostr relays in `mountWallet()`. While Aegis is waiting for first use, +**nobody listens on those relays**. A paired dapp that sends a sign request +gets no answer until something else wakes Aegis. Aegis must tell the host: + +```js +// after any change to the persisted pairing set (persist(), disconnect, removeWallet) +const live = [...uris.values()].some((s) => s.size > 0); +api.startAtLaunch(live); +``` + +Call it once at startup as well, after the pairings are restored, so a +profile that already has pairings flips to startup on the next launch. Guard +it with `typeof api.startAtLaunch === "function"` for older hosts. + +### 4. Other work that only runs while Aegis runs (decide, no fix required) + +- Price feed polling (`prices.js`, opt-in) and per-chain adapter refresh / + electrum subscriptions do not run until first use. Balances are fetched + when the panel opens, which already happens today for a locked vault. +- `tryAutoUnlock` (the safeStorage "stay unlocked" session) now runs at + first use, not at launch. It unlocks the shared password vault, so + Settings › Passwords also stays locked until then. If that matters, the + "stay unlocked" option should also call `api.startAtLaunch(true)`. +- `installStorageCache(api)` and `migrateLegacyStorage(api)` are cheap and + just move to first use. + +### 5. Check after switching + +- Fresh profile, launch: the log shows `aegis v… starts on first use`, the + dock shows the Wallet button, and `scripts/boot-trace/run.mjs` shows the + `addons` column without Aegis. +- On a dapp: `window.ethereum` exists at page load. The first + `eth_requestAccounts` starts Aegis (`started aegis on first use (page + eth.requestAccounts)`) and shows the approval. +- With a locked vault: the first page call fails with the usual locked + message right away, not after 5 s. +- A `wiz://` link click on a cold Aegis opens the pairing approval. +- After pairing a WC dapp, `settings.json` → `addonsStartAtLaunch` contains + `aegis`. The next launch logs `activated aegis` and the paired dapp's + requests arrive.