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().
147 lines
7.3 KiB
Markdown
147 lines
7.3 KiB
Markdown
# 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.
|