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);