diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index 37c555c5..a309ea62 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.8.6", + "version": "0.8.7", "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-generic-imported.js b/bundled-addons/aegis/lib/chain-generic-imported.js index c60a3681..c267a42b 100644 --- a/bundled-addons/aegis/lib/chain-generic-imported.js +++ b/bundled-addons/aegis/lib/chain-generic-imported.js @@ -10,6 +10,24 @@ module.exports = function makeGenericImportedAdapter() { + // base58check T… -> 41-prefixed hex, for comparing against the raw + // owner_address/to_address fields the /v1 tx feed returns. + const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + function tronAddrToHex(b58) { + try { + let n = 0n; + for (const ch of String(b58)) { + const i = B58.indexOf(ch); + if (i < 0) return ""; + n = n * 58n + BigInt(i); + } + let hex = n.toString(16); + if (hex.length % 2) hex = "0" + hex; + // 25 bytes = 21 payload + 4 checksum; drop the checksum. + return hex.padStart(50, "0").slice(0, 42); + } catch { return ""; } + } + const CHAIN_CFGS = { eth: { ticker: "ETH", decimals: 18, @@ -30,7 +48,11 @@ module.exports = function makeGenericImportedAdapter() { ticker: "TRX", decimals: 6, networks: { mainnet: { id: "mainnet", label: "Mainnet", rpc: "https://api.trongrid.io", explorerAddr: "https://tronscan.org/#/address/", explorerTx: "https://tronscan.org/#/transaction/" }, - nile: { id: "nile", label: "Nile testnet", rpc: "https://api.nileex.io", explorerAddr: "https://nile.tronscan.org/#/address/", explorerTx: "https://nile.tronscan.org/#/transaction/", testnet: true, faucet: "https://nileex.io/join/getJoinPage" }, + // nile.trongrid.io, NOT api.nileex.io: nileex only serves the + // /wallet/* JSON-RPC family and 404s the whole /v1/ REST family, + // which is where transaction history and the trc20 token list + // live. Balance worked, everything else silently came back empty. + nile: { id: "nile", label: "Nile testnet", rpc: "https://nile.trongrid.io", explorerAddr: "https://nile.tronscan.org/#/address/", explorerTx: "https://nile.tronscan.org/#/transaction/", testnet: true, faucet: "https://nileex.io/join/getJoinPage" }, }, // Tron HTTP API returns account.balance in SUN (10^-6 TRX). async fetchBalance({ rpc, address }) { @@ -40,6 +62,74 @@ module.exports = function makeGenericImportedAdapter() { const j = await r.json(); return String(j?.balance || 0); }, + async fetchHistory({ rpc, address }) { + const r = await fetch(`${rpc.replace(/\/+$/, "")}/v1/accounts/${encodeURIComponent(address)}/transactions?limit=25`); + if (!r.ok) throw new Error(`Tron history HTTP ${r.status}`); + const j = await r.json(); + const list = Array.isArray(j?.data) ? j.data : []; + return list.map((t) => { + const c = t?.raw_data?.contract?.[0]; + const v = c?.parameter?.value || {}; + const ownerHex = String(v.owner_address || ""); + // owner/to come back as 41-prefixed hex regardless of visible. + const mineHex = tronAddrToHex(address); + const outgoing = !!mineHex && ownerHex.toLowerCase() === mineHex.toLowerCase(); + const amount = Number(v.amount || 0); + const ok = Array.isArray(t.ret) ? t.ret[0]?.contractRet === "SUCCESS" : true; + return { + txid: t.txID || t.txid, + time: Math.floor((t.block_timestamp || t.raw_data?.timestamp || 0) / 1000), + confirmations: ok ? 1 : 0, + status: ok ? "confirmed" : "failed", + // Aegis renders `delta` in the wallet's base unit (sun here). + delta: c?.type === "TransferContract" ? (outgoing ? -amount : amount) : 0, + kind: c?.type || "Contract", + }; + }).filter((t) => t.txid); + }, + // TRC20 balances live on the /v1 REST family. The balance map is + // contract -> raw amount with no symbol/decimals, so we join it + // against token_info from recent transfers to name what we can. + async fetchTokens({ rpc, address }) { + const base = rpc.replace(/\/+$/, ""); + const r = await fetch(`${base}/v1/accounts/${encodeURIComponent(address)}`); + if (!r.ok) throw new Error(`Tron account HTTP ${r.status}`); + const j = await r.json(); + const acct = Array.isArray(j?.data) ? j.data[0] : j?.data; + const raw = Array.isArray(acct?.trc20) ? acct.trc20 : []; + const balances = new Map(); + for (const entry of raw) { + for (const [contract, amt] of Object.entries(entry || {})) { + if (String(amt) !== "0") balances.set(contract, String(amt)); + } + } + if (!balances.size) return []; + const info = new Map(); + try { + const tr = await fetch(`${base}/v1/accounts/${encodeURIComponent(address)}/transactions/trc20?limit=100`); + if (tr.ok) { + const tj = await tr.json(); + for (const t of (Array.isArray(tj?.data) ? tj.data : [])) { + const ti = t?.token_info; + if (ti && ti.address && !info.has(ti.address)) info.set(ti.address, ti); + } + } + } catch { /* names are a nicety; balances still render */ } + // Named tokens first: an address that's been airdrop-spammed can + // hold dozens of contracts we have no token_info for, and those + // would otherwise bury the ones the user actually cares about. + return Array.from(balances, ([contract, balance]) => { + const ti = info.get(contract); + return { + mint: contract, + symbol: ti?.symbol || "?", + name: ti?.name || "", + decimals: Number.isFinite(Number(ti?.decimals)) ? Number(ti.decimals) : 0, + known: !!ti, + balance, + }; + }).sort((a, b) => (b.known - a.known) || (a.symbol || "").localeCompare(b.symbol || "")); + }, }, sol: { ticker: "SOL", decimals: 9, @@ -72,6 +162,7 @@ module.exports = function makeGenericImportedAdapter() { this._state = { balance: { confirmed: "0", unconfirmed: "0" }, history: [], + tokens: [], scanning: false, error: null, }; @@ -95,6 +186,7 @@ module.exports = function makeGenericImportedAdapter() { addressPath: null, balance: this._state.balance, history: this._state.history, + tokens: this._state.tokens, scanning: this._state.scanning, error: this._state.error, server: this._net.rpc, @@ -109,9 +201,23 @@ module.exports = function makeGenericImportedAdapter() { async refresh() { this._state.scanning = true; this._emit(); + const opts = { rpc: this._net.rpc, address: this._address }; try { - const confirmed = await this._cfg.fetchBalance({ rpc: this._net.rpc, address: this._address }); + // Only the balance is load-bearing — history and tokens are + // best-effort so one 404 on a chain that has no keyless feed + // doesn't blank the wallet. + const [confirmed, history, tokens] = await Promise.all([ + this._cfg.fetchBalance(opts), + this._cfg.fetchHistory + ? this._cfg.fetchHistory(opts).catch((e) => { this.log("history failed:", e?.message || e); return null; }) + : Promise.resolve(null), + this._cfg.fetchTokens + ? this._cfg.fetchTokens(opts).catch((e) => { this.log("tokens failed:", e?.message || e); return null; }) + : Promise.resolve(null), + ]); this._state.balance = { confirmed: String(confirmed || 0), unconfirmed: "0" }; + if (Array.isArray(history)) this._state.history = history; + if (Array.isArray(tokens)) this._state.tokens = tokens; this._state.error = null; } catch (e) { this._state.error = e?.message || String(e); diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index 8feed09c..b8a52f0e 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -2941,6 +2941,33 @@ function renderTokens() { }).catch(() => {}); return; } + // TRX wallets: TRC20 balances. Read-only — Aegis has no TRC20 spend + // path yet, so there's no Send button here (unlike SPL above). + if (chain() === "trx") { + const tokens = s?.tokens || []; + card.hidden = tokens.length === 0; + const hintEl = $("tokensHint"); + if (hintEl) hintEl.textContent = "TRC20 tokens held by this wallet. Read-only in this build — use the explorer link to move them."; + if (!tokens.length) return; + el.innerHTML = tokens.map((t) => { + const dec = Number(t.decimals) || 0; + const short = String(t.mint || ""); + const addrLine = `${esc(short.slice(0, 10))}…${esc(short.slice(-6))}`; + // Unknown contracts have no decimals, so a raw integer would be a + // misleading "balance". Say so instead of inventing a number. + const title = t.known + ? `${esc(t.symbol)}${t.name ? ' ' + esc(t.name) + "" : ""}` + : `Unknown token`; + const amount = t.known + ? esc(fmtTokenAmount(t.balance, dec)) + : `${esc(t.balance)} raw`; + return `