From 74224cd7dd5471a414688dc200238b18db0fef9a Mon Sep 17 00:00:00 2001 From: Local Dev Date: Mon, 28 Sep 2026 22:52:15 +0200 Subject: [PATCH] =?UTF-8?q?fix(aegis):=200.15.0=20=E2=80=94=20ask=20Electr?= =?UTF-8?q?um=20for=20protocol=201.5,=20which=20is=20where=20the=20tokens?= =?UTF-8?q?=20were?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aegis negotiated a flat protocol "1.4". Measured against Fulcrum 2.1.0 on chipnet, for a wallet holding CashTokens: asked "1.4" -> negotiated 1.4 -> 39 utxos, 0 with token_data asked ["1.4","1.5.3"] -> negotiated 1.5.3 -> 130 utxos, 91 with token_data So 1.4 did not merely omit the `token_data` field — Fulcrum left the token-bearing outputs out of listunspent altogether. Ninety-one UTXOs were invisible to the wallet, along with the BCH sitting in them. That is a balance-correctness bug, not only a missing Assets card, and it applied to the HD path too, since lib/wallet.js reads the same listunspent. Now a [min, max] range: a modern server picks 1.5.3, an older one still settles on 1.4, so nothing that worked before stops working. The negotiated version is recorded on the client for diagnosis. Second half: the imported BCH adapter had no CashToken code at all — it never set tokenBalances and snapshot() never exposed it, so the panel's Assets card was hidden for every WIF import however many tokens the address held. A chipnet test wallet with 46 categories showed nothing. It now aggregates from the server's own token_data, which costs one call for the whole set rather than the per-UTXO transaction fetch the HD path uses, and emits the same serialised shape the panel already reads. Verified against that wallet: 130 UTXOs, 46 categories, 17 fungible, 32 with NFTs, JSON-clean. Found because the user said their asset "uses a different asset category" and suggested checking with the explorer. It is ordinary CashTokens; the wallet simply could not see them. My earlier conclusion that the empty Assets card was correct came from probing a single address that genuinely holds no tokens and generalising from it. --- bundled-addons/aegis/addon.json | 2 +- .../aegis/lib/chain-bch-imported.js | 43 ++++++++++++++++++- bundled-addons/aegis/lib/electrum.js | 13 +++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index 69e24e31..d21aef05 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.14.0", + "version": "0.15.0", "category": "plugin", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.", "author": "Silent Mode", diff --git a/bundled-addons/aegis/lib/chain-bch-imported.js b/bundled-addons/aegis/lib/chain-bch-imported.js index 82557b1d..c65505c9 100644 --- a/bundled-addons/aegis/lib/chain-bch-imported.js +++ b/bundled-addons/aegis/lib/chain-bch-imported.js @@ -120,6 +120,7 @@ module.exports = function makeImportedBchAdapter({ history: [], utxos: [], height: 0, + tokenBalances: null, // { : {fungible, nfts, utxoCount} } scanning: false, error: null, }; @@ -148,6 +149,9 @@ module.exports = function makeImportedBchAdapter({ balance: this._state.balance, height: this._state.height, history: this._state.history, + // Without this the panel's Assets card has nothing to read and stays + // hidden however many tokens refresh() found. + tokenBalances: this._state.tokenBalances || null, scanning: this._state.scanning, error: this._state.error, server: this._client.url || null, @@ -170,9 +174,46 @@ module.exports = function makeImportedBchAdapter({ this._state.balance = { confirmed: Number(bal?.confirmed || 0), unconfirmed: Number(bal?.unconfirmed || 0) }; // Always pull UTXOs so spend / send-max work off fresh state. const utxos = await this._client.call("blockchain.scripthash.listunspent", [this._scripthash]); - this._state.utxos = (Array.isArray(utxos) ? utxos : []).map((u) => ({ + const list = Array.isArray(utxos) ? utxos : []; + this._state.utxos = list.map((u) => ({ txid: u.tx_hash, vout: u.tx_pos, value: Number(u.value), height: Number(u.height || 0), + token: u.token_data || null, })); + // CashTokens. This adapter never looked for them, so a WIF-imported + // wallet holding tokens reported none and the panel hid its Assets + // card — a chipnet test wallet with 46 categories showed nothing. + // + // The HD path (lib/wallet.js) decodes the token prefix off each + // UTXO's scriptPubKey, which costs one transaction fetch per UTXO. + // Here we take the server's own `token_data` instead: one call for + // the whole set. That arrives only when protocol >= 1.5 was + // negotiated (see electrum.js), and when it does not, tokens are + // simply absent — the same as before this change, never wrong. + const tokenBalances = {}; + for (const u of list) { + const t = u.token_data; + if (!t || !t.category) continue; + const cat = String(t.category); + if (!tokenBalances[cat]) tokenBalances[cat] = { fungible: 0n, nfts: [], utxoCount: 0 }; + const b = tokenBalances[cat]; + b.utxoCount++; + if (t.amount) { try { b.fungible += BigInt(t.amount); } catch (_e) {} } + if (t.nft) { + b.nfts.push({ + utxoId: `${u.tx_hash}:${u.tx_pos}`, + commitmentHex: t.nft.commitment || "", + capability: t.nft.capability || "none", + capabilityLabel: t.nft.capability || "none", + }); + } + } + // Same serialised shape the HD wallet emits — fungible as a decimal + // string, since JSON.stringify cannot carry a BigInt. + const serialised = {}; + for (const [cat, b] of Object.entries(tokenBalances)) { + serialised[cat] = { fungible: b.fungible.toString(), nfts: b.nfts, utxoCount: b.utxoCount }; + } + this._state.tokenBalances = serialised; if (full) { const hist = await this._client.call("blockchain.scripthash.get_history", [this._scripthash]); this._state.history = (hist || []).slice(-50).map((h) => ({ diff --git a/bundled-addons/aegis/lib/electrum.js b/bundled-addons/aegis/lib/electrum.js index 88f0859d..3a8141d6 100644 --- a/bundled-addons/aegis/lib/electrum.js +++ b/bundled-addons/aegis/lib/electrum.js @@ -22,7 +22,18 @@ module.exports = function makeElectrum({ WebSocket, log = () => {} }) { this.ws = ws; const fail = (e) => { if (!this.closed) { this.closed = true; reject(e instanceof Error ? e : new Error("electrum ws error: " + this.url)); } }; ws.on("open", async () => { - try { await this.call("server.version", ["theseus-bchwallet", "1.4"]); resolve(this); } + // A RANGE, not a flat "1.4". Fulcrum only attaches `token_data` to + // listunspent results once protocol >= 1.5 is negotiated, and with + // a flat 1.4 it silently omits it — which is why imported BCH + // wallets showed no CashTokens at all. A [min, max] pair lets a + // modern server pick 1.5.3 while an older one still settles on 1.4, + // so nothing that worked before stops working. + try { + const v = await this.call("server.version", ["theseus-bchwallet", ["1.4", "1.5.3"]]); + this.serverVersion = Array.isArray(v) ? v[0] : null; + this.protocolVersion = Array.isArray(v) ? v[1] : null; + resolve(this); + } catch (e) { fail(e); this.close(); } }); ws.on("error", fail);