theseus/NOTE-aegis-on-demand.md
Local Dev 434f6d35d0 Theseus: hand-off for moving Aegis to on-demand activation
Aegis is most of the remaining launch cost: ~165 ms of synchronous
activation, then ~610 ms of main-thread work loading its crypto and
WizardConnect deps. It was left on startup because another session
owns bundled-addons/aegis. NOTE-aegis-on-demand.md says exactly what it
needs, measured against 0.27.1:

- addon.json: "activation": "on-demand", plus panels [{id:"main",
  title:"Wallet", page:"panel.html"}].
- activate() must return a promise that resolves once deps are loaded
  and, only if the vault is already unlocked, wallets are mounted.
  vault.derive never rejects on a locked vault, so awaiting
  mountAllWallets there would stall every first call for the full 5 s
  host wait.
- Persisted WizardConnect pairings (wc/<walletId>/uris) need a relay
  listener: call api.startAtLaunch(true) while any exist, false when
  none are left.
- Bridge calls, panel open, wiz:// links and Settings calls already wake
  it. The dapp globals are injected from the manifest before it runs.

GOTCHAS: an enabled add-on may not be running, so code in main reaches
add-ons through dispatch() rather than checking isActive()/hasHandler().
2026-10-03 16:05:41 +02:00

147 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<walletId>/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.