diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index 118579ae..32913145 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.27.0", + "version": "0.27.1", "category": "plugin", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash and window.wizardconnect on any site; window.tronWeb / window.tronLink / window.ethereum / window.solana too. Every call needs your approval.", "author": "Silent Mode", diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index bc2f2358..7a4163c2 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -417,6 +417,35 @@ function coinsForPanel() { // ---- wallet list ------------------------------------------------------------ +// Replace api.storage with a write-through read cache. Writes still go to the +// host so nothing changes about durability or who owns the file; only the +// repeated parsing of it disappears. Falls back to the host's storage +// untouched if the api object will not accept the substitution. +function installStorageCache(api) { + const raw = api.storage; + if (!raw || typeof raw.all !== "function") return false; + let mem = null; + const load = () => { + if (!mem) { try { mem = raw.all() || {}; } catch { mem = {}; } } + return mem; + }; + const cached = { + get: (key, fallback = null) => { + const s = load(); + return Object.prototype.hasOwnProperty.call(s, key) ? s[key] : fallback; + }, + set: (key, value) => { + load()[key] = value; + raw.set(key, value); + }, + all: () => ({ ...load() }), + }; + try { + api.storage = cached; + return api.storage === cached; + } catch { return false; } +} + function readWallets(api) { const raw = api.storage.get("wallets", null); return Array.isArray(raw) ? raw : null; @@ -3223,6 +3252,17 @@ module.exports = { // so the toolbar dock button renders the aegis.x brand mark instead // of a fallback emoji. api.registerSidebarPanel({ id: "main", title: "Wallet", page: "panel.html" }); + // Serve reads from memory. The host's storage.get() does a readFileSync + // plus a JSON.parse of the ENTIRE add-on store on every single call, on + // the Electron main thread. fullState() alone reads it seven-odd times + // (selectedWalletId, walletRoles, walletEntries, snapshotForSelected, + // the server list...) and emitState() runs on every adapter change, so a + // large store made opening Aegis hang the whole browser. + // + // Safe because this process is the only writer: Aegis's panel talks to + // the host over addon messages and never touches addon storage directly. + // If that ever changes, this cache has to go or be invalidated. + installStorageCache(api); const c = ctx = { api, d: null, diff --git a/bundled-addons/aegis/lib/wallet.js b/bundled-addons/aegis/lib/wallet.js index 7ab3b373..fb6cff8c 100644 --- a/bundled-addons/aegis/lib/wallet.js +++ b/bundled-addons/aegis/lib/wallet.js @@ -35,13 +35,36 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora // indistinguishable from "no token", so they are dropped once rather than // trusted. Only the pre-1.5 fallback path reads this for classification, // but a warm v1 cache there would silently report a token wallet as empty. - const TX_CACHE_VERSION = 2; + // v3 because v2 was unbounded. getTx() kept every transaction it ever + // fetched, with full vin/vout arrays, and a busy chipnet test wallet grew + // this to 7.7 MB. That matters enormously, because the host's addon store + // is ONE JSON file per add-on that is read, parsed, stringified and written + // whole and SYNCHRONOUSLY on the Electron main thread — the thread that + // drives the entire browser. A 7.7 MB cache turned every storage access + // into a multi-hundred-millisecond freeze of all of Theseus. Bumping the + // version also discards existing oversized caches on first load. + const TX_CACHE_VERSION = 3; + const TX_CACHE_MAX = 400; let txCache = storage.get("txCache", {}) || {}; if (storage.get("txCacheVersion", 1) !== TX_CACHE_VERSION) { txCache = {}; storage.set("txCache", txCache); storage.set("txCacheVersion", TX_CACHE_VERSION); } + // Only write when something actually changed. loadHistory() used to persist + // the cache on every refresh, so an idle wallet rewrote the whole file on + // every poll for nothing. + let txDirty = false; + function pruneTxCache() { + const ids = Object.keys(txCache); + if (ids.length <= TX_CACHE_MAX) return; + // Newest first by block time. Unconfirmed entries have time 0 but are by + // definition current, so they sort as newest rather than being evicted + // first. Anything dropped is re-fetchable on demand. + ids.sort((a, b) => ((txCache[b].time || Infinity) - (txCache[a].time || Infinity))); + for (const id of ids.slice(TX_CACHE_MAX)) delete txCache[id]; + txDirty = true; + } let refreshTimer = null; let subscribedHeaders = false; @@ -230,6 +253,7 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora size: raw.size || 0, }; txCache[txid] = slim; + txDirty = true; return slim; } @@ -271,7 +295,7 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora }); } state.history = out; - storage.set("txCache", txCache); + if (txDirty) { pruneTxCache(); storage.set("txCache", txCache); txDirty = false; } } function scriptToAddress(scriptHex) {