chore(aegis): 0.9.2 — Receive tab reorder, one balance, token history

Receive was ordered QR → address → tokens, so 200px of always-on QR sat
above the thing people came for and pushed the asset list off screen.

- Address first, with Copy and a QR button; the QR expands inline and
  the choice sticks, because someone who receives by QR wants it every
  time and someone who copies never does.
- Explorer and Faucet moved up to the header status row. They act on the
  selected wallet, not on the act of receiving, and down there they
  competed with Copy for the one row that gets used.
- Assets card renamed from Tokens and now leads with the native coin, so
  "what does this wallet hold" is one list rather than two places.
- "+ Add another <TICKER>" moved below the address list.

The balance appeared three times — header, drilldown subtitle, address
row. Now once in the header; the subtitle keeps only the per-unit price,
and the per-address amount returns when a coin actually has more than one
address to compare. The address row drops its truncated address (the full
one is at the top of Receive) and keeps the wallet name.

The per-address asset list added in 0.8.8 duplicated the Assets card and
is removed — assets live in one place.

Token amounts were unreadable: an 18-decimal balance rendered as
60000000.000000005435817984. Capped to 8 decimals with thousands
separators, exact value on hover.

A symbol claimed by more than one contract now carries a LOOK-ALIKE tag.
The test wallet holds four different contracts all calling themselves
"Test USDT" — spam mints borrowing a trusted ticker so a careless send
lands on the wrong one. We can't tell which is genuine, so we mark every
member of the clash rather than guessing.

History showed native transfers only, so a wallet that had only ever
moved USDT looked empty. TRC20 transfers are merged in newest-first, each
carrying its own decimals and ticker (rendering a token against the
chain's scale would be off by orders of magnitude). Both feeds are on by
default; the checkboxes narrow rather than opt in, and the last one
checked can't be unchecked into an empty list.
This commit is contained in:
Local Dev 2026-09-23 01:17:33 +02:00
parent 7a71b08cf7
commit 1be352dad6
4 changed files with 220 additions and 95 deletions

View file

@ -1,7 +1,7 @@
{ {
"id": "aegis", "id": "aegis",
"name": "Aegis Wallet", "name": "Aegis Wallet",
"version": "0.9.1", "version": "0.9.2",
"category": "plugin", "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.", "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", "author": "Silent Mode",

View file

@ -150,7 +150,45 @@ module.exports = function makeGenericImportedAdapter() {
const j = await r.json(); const j = await r.json();
return String(j?.balance || 0); return String(j?.balance || 0);
}, },
async fetchHistory({ rpc, address }) { // Native TRX transfers AND TRC20 token transfers, merged newest-first
// and each tagged with `asset` so the History tab can filter. Token
// movement is invisible in the native feed — a wallet that only ever
// moved USDT looked like it had no history at all.
async fetchHistory(opts) {
const [native, tokens] = await Promise.all([
CHAIN_CFGS.trx._fetchNativeHistory(opts),
CHAIN_CFGS.trx._fetchTokenHistory(opts).catch(() => []),
]);
return [...native, ...tokens]
.sort((a, b) => (b.time || 0) - (a.time || 0))
.slice(0, 40);
},
async _fetchTokenHistory({ rpc, address }) {
const r = await fetch(`${rpc.replace(/\/+$/, "")}/v1/accounts/${encodeURIComponent(address)}/transactions/trc20?limit=25`);
if (!r.ok) throw new Error(`Tron trc20 history HTTP ${r.status}`);
const j = await r.json();
const list = Array.isArray(j?.data) ? j.data : [];
const me = String(address);
return list.map((t) => {
const ti = t.token_info || {};
const outgoing = String(t.from || "") === me;
const raw = String(t.value || "0");
return {
txid: t.transaction_id,
time: Math.floor(Number(t.block_timestamp || 0) / 1000),
confirmations: 1,
status: "confirmed",
// Token amounts are in the TOKEN's own decimals, not the
// chain's, so they travel with their own scale rather than
// being rendered against the native one.
delta: (outgoing ? "-" : "") + raw,
assetDecimals: Number.isFinite(Number(ti.decimals)) ? Number(ti.decimals) : 0,
asset: cleanTokenText(ti.symbol) || "token",
kind: "TRC20",
};
}).filter((t) => t.txid);
},
async _fetchNativeHistory({ rpc, address }) {
const r = await fetch(`${rpc.replace(/\/+$/, "")}/v1/accounts/${encodeURIComponent(address)}/transactions?limit=25`); const r = await fetch(`${rpc.replace(/\/+$/, "")}/v1/accounts/${encodeURIComponent(address)}/transactions?limit=25`);
if (!r.ok) throw new Error(`Tron history HTTP ${r.status}`); if (!r.ok) throw new Error(`Tron history HTTP ${r.status}`);
const j = await r.json(); const j = await r.json();
@ -171,6 +209,7 @@ module.exports = function makeGenericImportedAdapter() {
status: ok ? "confirmed" : "failed", status: ok ? "confirmed" : "failed",
// Aegis renders `delta` in the wallet's base unit (sun here). // Aegis renders `delta` in the wallet's base unit (sun here).
delta: c?.type === "TransferContract" ? (outgoing ? -amount : amount) : 0, delta: c?.type === "TransferContract" ? (outgoing ? -amount : amount) : 0,
asset: null, // null = the chain's native coin
kind: c?.type || "Contract", kind: c?.type || "Contract",
}; };
}).filter((t) => t.txid); }).filter((t) => t.txid);

View file

@ -35,8 +35,8 @@
.bal { margin-top: 8px; font-variant-numeric: tabular-nums; } .bal { margin-top: 8px; font-variant-numeric: tabular-nums; }
.bal .big { font-size: 22px; font-weight: 650; letter-spacing: .2px; } .bal .big { font-size: 22px; font-weight: 650; letter-spacing: .2px; }
.bal .big small { font-size: 13px; color: var(--mut); font-weight: 500; margin-left: 4px; } .bal .big small { font-size: 13px; color: var(--mut); font-weight: 500; margin-left: 4px; }
.bal .sub { color: var(--dim); font-size: 11.5px; display: flex; justify-content: space-between; gap: 8px; } .bal .sub { color: var(--dim); font-size: 11.5px; display: flex; align-items: center; gap: 8px; }
.bal .sub .netlbl { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bal .sub .netlbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* Full-panel sheet — fills most of the sidebar but stops above the /* Full-panel sheet — fills most of the sidebar but stops above the
footer so the aegis.x brand + version chip stay visible. Users footer so the aegis.x brand + version chip stay visible. Users
opening + should still know which build they're on and be able opening + should still know which build they're on and be able
@ -79,6 +79,16 @@
#drop .brnet.on { background: rgb(from var(--acid, #d6ff3d) r g b / .14); border-color: rgb(from var(--acid, #d6ff3d) r g b / .55); #drop .brnet.on { background: rgb(from var(--acid, #d6ff3d) r g b / .14); border-color: rgb(from var(--acid, #d6ff3d) r g b / .55);
color: var(--acid, #d6ff3d); font-weight: 600; } color: var(--acid, #d6ff3d); font-weight: 600; }
#drop .brnet .hint { color: currentColor; opacity: .65; font-size: 11px; } #drop .brnet .hint { color: currentColor; opacity: .65; font-size: 11px; }
.histfilter { display: flex; align-items: center; gap: 14px; padding: 0 2px 10px;
border-bottom: 1px solid var(--line); margin-bottom: 8px; font-size: 12px; }
.histfilter[hidden] { display: none; }
/* Assets card rows (0.9.2). The native coin leads the list; a symbol
claimed by several contracts gets a LOOK-ALIKE tag so a borrowed
ticker can't pass for the real token. */
.tx.asset { padding: 7px 8px; border-radius: 6px; }
.tx.asset:hover { background: rgba(255,255,255,.04); }
.tx.asset.native { background: rgb(from var(--acid, #d6ff3d) r g b / .06); }
.ttag.tdupe { background: rgba(224,90,90,.18); color: #f6768a; cursor: help; }
.ttag { display: inline-block; font-size: 9.5px; letter-spacing: .06em; padding: 1px 5px; border-radius: 3px; .ttag { display: inline-block; font-size: 9.5px; letter-spacing: .06em; padding: 1px 5px; border-radius: 3px;
background: rgba(224,179,65,.18); color: #e0b341; font-weight: 700; vertical-align: middle; margin-left: 2px; } background: rgba(224,179,65,.18); color: #e0b341; font-weight: 700; vertical-align: middle; margin-left: 2px; }
/* 0.8.4: hNet became a proper chip on its own row (.netrow) so the /* 0.8.4: hNet became a proper chip on its own row (.netrow) so the
@ -99,6 +109,13 @@
#drop .catgroup { padding: 4px 6px 2px 10px; color: var(--dim); font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; } #drop .catgroup { padding: 4px 6px 2px 10px; color: var(--dim); font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; }
#drop .row.unowned { opacity: .8; } #drop .row.unowned { opacity: .8; }
#drop .row.unowned .v { color: var(--acid, #d6ff3d); font-weight: 600; font-size: 11px; } #drop .row.unowned .v { color: var(--acid, #d6ff3d); font-weight: 600; font-size: 11px; }
/* Per-wallet actions promoted into the header's status row (0.9.2). */
.hdracts { margin-left: auto; display: inline-flex; gap: 4px; }
.hdract { background: transparent; border: 1px solid var(--line); color: var(--dim);
border-radius: 999px; padding: 1px 9px; font: inherit; font-size: 10.5px;
cursor: pointer; line-height: 1.5; }
.hdract:hover { border-color: var(--acid, #d6ff3d); color: var(--acid, #d6ff3d); }
.hdract[hidden] { display: none; }
.fiat { color: var(--dim); font-size: 12.5px; margin-left: 10px; font-weight: 500; letter-spacing: .2px; } .fiat { color: var(--dim); font-size: 12.5px; margin-left: 10px; font-weight: 500; letter-spacing: .2px; }
.portfolio { margin-top: 6px; color: var(--mut); font-size: 11.5px; } .portfolio { margin-top: 6px; color: var(--mut); font-size: 11.5px; }
.portfolio b { color: var(--ink); font-weight: 600; } .portfolio b { color: var(--ink); font-weight: 600; }
@ -198,7 +215,8 @@
.btn.danger { color: var(--danger); } .btn.danger { color: var(--danger); }
.btn.sm { padding: 4px 9px; font-size: 12px; } .btn.sm { padding: 4px 9px; font-size: 12px; }
.actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; } .actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.qrwrap { display: grid; place-items: center; padding: 12px; background: #fff; border-radius: 10px; margin-bottom: 12px; } .qrwrap { display: grid; place-items: center; padding: 12px; background: #fff; border-radius: 10px; margin-top: 12px; }
.qrwrap[hidden] { display: none; }
canvas { image-rendering: pixelated; } canvas { image-rendering: pixelated; }
input[type=text], input[type=number], input[type=password], textarea { width: 100%; padding: 7px 9px; border-radius: 7px; background: var(--panel); input[type=text], input[type=number], input[type=password], textarea { width: 100%; padding: 7px 9px; border-radius: 7px; background: var(--panel);
border: 1px solid var(--line); color: var(--ink); font: inherit; font-size: 13px; outline: none; } border: 1px solid var(--line); color: var(--ink); font: inherit; font-size: 13px; outline: none; }
@ -567,6 +585,14 @@
<div class="sub"> <div class="sub">
<span class="netlbl" id="netlbl">connecting…</span> <span class="netlbl" id="netlbl">connecting…</span>
<span class="net"><span class="dot" id="dot"></span></span> <span class="net"><span class="dot" id="dot"></span></span>
<!-- 0.9.2: Explorer / Faucet moved up out of the Receive address
card. They act on the selected wallet, not on the act of
receiving, and down there they competed with Copy + QR for
the one row people actually use. -->
<span class="hdracts">
<button class="hdract" id="viewAddr" type="button" title="Open this address in a block explorer">Explorer</button>
<button class="hdract" id="openFaucet" type="button" title="Open this testnet's faucet" hidden>Faucet</button>
</span>
</div> </div>
<div id="portfolio" class="portfolio" hidden></div> <div id="portfolio" class="portfolio" hidden></div>
</div> </div>
@ -595,19 +621,22 @@
<button data-rcv-mode="consolidate" type="button">Consolidate <span id="rcvConsolidateCount" class="hint"></span></button> <button data-rcv-mode="consolidate" type="button">Consolidate <span id="rcvConsolidateCount" class="hint"></span></button>
</div> </div>
<div id="rcvNormal"> <div id="rcvNormal">
<div class="qrwrap"><canvas id="qr" width="200" height="200"></canvas></div> <!-- 0.9.2: address first, assets next, QR last and collapsed.
The QR was 200px of always-on vertical space above the thing
people actually came for, and it pushed the token list below
the fold. Explorer / Faucet moved to the header. -->
<div class="card"> <div class="card">
<div class="lbl">Receiving address <span id="addrMeta"></span></div> <div class="lbl">Receiving address <span id="addrMeta"></span></div>
<div class="mono" id="addr">—</div> <div class="mono" id="addr">—</div>
<div class="actions"> <div class="actions">
<button class="btn sm" id="copyAddr">Copy</button> <button class="btn sm" id="copyAddr">Copy</button>
<button class="btn sm" id="qrToggle" aria-expanded="false">QR</button>
<button class="btn sm" id="nextAddr" hidden>Next unused address</button> <button class="btn sm" id="nextAddr" hidden>Next unused address</button>
<button class="btn sm" id="viewAddr">Explorer</button>
<button class="btn sm" id="openFaucet" hidden>Faucet</button>
</div> </div>
<div class="qrwrap" id="qrWrap" hidden><canvas id="qr" width="200" height="200"></canvas></div>
</div> </div>
<div class="card" id="tokensCard" hidden style="margin-top:12px"> <div class="card" id="tokensCard" hidden style="margin-top:12px">
<div class="lbl">Tokens</div> <div class="lbl">Assets <span class="hint" id="tokensCount"></span></div>
<div id="tokensList" class="kv"></div> <div id="tokensList" class="kv"></div>
<div class="hint" id="tokensHint">Tokens held by this wallet.</div> <div class="hint" id="tokensHint">Tokens held by this wallet.</div>
</div> </div>
@ -679,6 +708,14 @@
</div> </div>
</section> </section>
<section id="tab-history" hidden> <section id="tab-history" hidden>
<!-- 0.9.2: token movement used to be invisible here — a wallet that
only ever moved USDT looked like it had no history. Both feeds
are on by default; the filters exist to narrow, not to opt in. -->
<div class="histfilter" id="histFilter" hidden>
<label class="switch"><input type="checkbox" id="histNative" checked><span id="histNativeLbl">Coin</span></label>
<label class="switch"><input type="checkbox" id="histTokens" checked><span>Tokens</span></label>
<span class="hint" id="histCount" style="margin-left:auto"></span>
</div>
<div id="txlist"></div> <div id="txlist"></div>
</section> </section>
<section id="tab-settings" hidden> <section id="tab-settings" hidden>

View file

@ -2463,46 +2463,8 @@ function wireStripDragDrop(el, chainGroups) {
// Inline replacement for the modal address list. Rendered directly into // Inline replacement for the modal address list. Rendered directly into
// the wallet strip element when stripView.mode === "addresses". Header // the wallet strip element when stripView.mode === "addresses". Header
// row has a back arrow (returns to the coins summary) and the coin's // row has a back arrow (returns to the coins summary) and the coin's
// Which address rows have their asset list expanded, in the coin drilldown. // name/logo; body rows show one wallet each with an inline ✎ / 🗑.
// Panel-session only — a drilldown is a transient view, so there's nothing // Assets live in the Receive tab's Assets card, not here — see 0.9.2.
// worth persisting across restarts.
const expandedAddrAssets = new Set();
// Normalise a wallet's assets into one row shape the drilldown can render,
// regardless of which chain family it came from:
// account-model (TRC20 / SPL) → w.tokens: [{mint, symbol, name, decimals, balance}]
// BCH CashTokens → w.tokenBalances: { <category>: {fungible, nfts[]} }
// Returns [{ id, symbol, name, amount }] with amount already formatted.
function assetsForWallet(w) {
const out = [];
for (const t of (w.tokens || [])) {
const dec = Number(t.decimals) || 0;
out.push({
id: String(t.mint || ""),
symbol: t.symbol || "?",
name: t.name || "",
// Unknown contracts have no decimals — show the raw integer rather
// than a number invented from an assumed scale.
amount: t.known === false ? `${t.balance} raw` : fmtTokenAmount(t.balance, dec),
});
}
const tb = w.tokenBalances || {};
for (const cat of Object.keys(tb)) {
const b = tb[cat] || {};
const fungible = String(b.fungible || "0");
const nftCount = Array.isArray(b.nfts) ? b.nfts.length : 0;
const parts = [];
// Decimals live in BCMR, which the drilldown doesn't fetch — show the
// raw fungible amount and let the Tokens card do the named rendering.
if (fungible !== "0") parts.push(fungible);
if (nftCount) parts.push(`${nftCount} NFT${nftCount === 1 ? "" : "s"}`);
if (!parts.length) continue;
out.push({ id: cat, symbol: "CashToken", name: "", amount: parts.join(" · ") });
}
return out;
}
// name/logo; body rows show one wallet each with balance + inline ✎ / 🗑.
function renderInlineCoinList(el, groupKey, group) { function renderInlineCoinList(el, groupKey, group) {
const { meta, wallets: gw } = group; const { meta, wallets: gw } = group;
const selId = state?.selectedWalletId; const selId = state?.selectedWalletId;
@ -2514,6 +2476,7 @@ function renderInlineCoinList(el, groupKey, group) {
const totalNative = fmtBig(totalUnits || 0, decimals); const totalNative = fmtBig(totalUnits || 0, decimals);
const totalUsd = usdOf(chain, totalUnits || 0, decimals); const totalUsd = usdOf(chain, totalUnits || 0, decimals);
const totalFiat = totalUsd != null ? fmtFiat(totalUsd) : ""; const totalFiat = totalUsd != null ? fmtFiat(totalUsd) : "";
const multiAddr = gw.length > 1;
const rows = gw.map((w) => { const rows = gw.map((w) => {
const on = w.id === selId ? "on" : ""; const on = w.id === selId ? "on" : "";
const units = walletBalanceUnits(w); const units = walletBalanceUnits(w);
@ -2535,39 +2498,31 @@ function renderInlineCoinList(el, groupKey, group) {
const shortName = shortLabel(w.label || "", 8); const shortName = shortLabel(w.label || "", 8);
// Fiat is dropped from the row to keep everything on one line; the // Fiat is dropped from the row to keep everything on one line; the
// aggregate coin fiat still shows in the drilldown header above. // aggregate coin fiat still shows in the drilldown header above.
// 0.8.8: per-address assets. Each row can expand to show what THIS // 0.9.2: the per-address asset list added in 0.8.8 duplicated the
// address holds beyond the native coin — TRC20/SPL via `tokens`, BCH // Receive tab's Assets card, so it's gone — assets live in one place.
// CashTokens via `tokenBalances`. Rows with nothing extra get no // The truncated address is gone too: the full one sits at the top of
// chevron so the list stays quiet for plain wallets. // Receive, and a shortened copy here was a second, less useful
const assets = assetsForWallet(w); // rendering of the same string. The row now carries identity (name)
const expanded = expandedAddrAssets.has(w.id); // and, only when there is more than one address to compare, balance.
const assetChip = assets.length
? `<button class="waassets ${expanded ? "on" : ""}" data-lpassets="${esc(w.id)}" title="${assets.length} asset${assets.length === 1 ? "" : "s"} on this address">${assets.length} <span class="wgchev">${expanded ? "▾" : "▸"}</span></button>`
: "";
const canRemoveRow = !(w.isDefault || w.isLegacy); const canRemoveRow = !(w.isDefault || w.isLegacy);
const rowRemove = canRemoveRow const rowRemove = canRemoveRow
? `<button class="wact wactdel" data-lpremove="${esc(w.id)}" title="Remove this wallet from Aegis">🗑</button>` ? `<button class="wact wactdel" data-lpremove="${esc(w.id)}" title="Remove this wallet from Aegis">🗑</button>`
: `<span class="wact" title="Default wallet — cannot be removed" style="opacity:.35;cursor:not-allowed">🔒</span>`; : `<span class="wact" title="Default wallet — cannot be removed" style="opacity:.35;cursor:not-allowed">🔒</span>`;
const assetRows = expanded && assets.length // With a single address the amount is identical to the header's
? `<div class="waassetlist">${assets.map((a) => ` // balance two rows up; showing it a third time is noise.
<div class="waasset"> const amountCell = multiAddr
<span class="waaname">${esc(a.symbol)}${a.name ? ` <span class="hint">${esc(a.name)}</span>` : ""}</span> ? `<span class="waamt"><span>${esc(bal)}</span><span class="watkr">${esc(meta.ticker)}</span></span>`
<span class="waaid mono" title="${esc(a.id)}">${esc(a.id.slice(0, 8))}…${esc(a.id.slice(-6))}</span> : `<span class="waamt"></span>`;
<span class="waaamt">${esc(a.amount)}</span>
</div>`).join("")}</div>`
: "";
return `<div class="warow ${on}" data-listpick="${esc(w.id)}" title="${esc(w.label || fullAddr)}"> return `<div class="warow ${on}" data-listpick="${esc(w.id)}" title="${esc(w.label || fullAddr)}">
<span class="wcell">${logoSvg(meta.logo, 14)}</span> <span class="wcell">${logoSvg(meta.logo, 14)}</span>
<span class="waaddr" title="${esc(fullAddr)}">${esc(displayAddr || "—")}</span> <span class="walabel walabel-wide" title="${esc(w.label || "")}">${esc(w.label || shortName || "—")}</span>
${fullAddr ? `<button class="wacopy" data-lpcopy="${esc(fullAddr)}" title="Copy full address (${esc(fullAddr)})">📋</button>` : `<span></span>`} ${fullAddr ? `<button class="wacopy" data-lpcopy="${esc(fullAddr)}" title="Copy full address (${esc(fullAddr)})">📋</button>` : `<span></span>`}
<span class="walabel" title="${esc(w.label || "")}">${esc(shortName)}</span> ${amountCell}
<span class="waamt"><span>${esc(bal)}</span><span class="watkr">${esc(meta.ticker)}</span></span>
<span style="display:inline-flex;gap:2px;justify-self:end;align-items:center"> <span style="display:inline-flex;gap:2px;justify-self:end;align-items:center">
${assetChip}
<button class="wact" data-lpedit="${esc(w.id)}" title="Rename / derivation path / remove">✎</button> <button class="wact" data-lpedit="${esc(w.id)}" title="Rename / derivation path / remove">✎</button>
${rowRemove} ${rowRemove}
</span> </span>
</div>${assetRows}`; </div>`;
}).join(""); }).join("");
// Surface any adapter errors from the wallets in this group. If a fetch // Surface any adapter errors from the wallets in this group. If a fetch
// is failing (RPC unreachable, CORS block, rate limit) the display would // is failing (RPC unreachable, CORS block, rate limit) the display would
@ -2576,9 +2531,6 @@ function renderInlineCoinList(el, groupKey, group) {
const errs = gw.filter((w) => w.error).map((w) => `${shortLabel(w.label || w.address || "?", 6)}: ${w.error}`); const errs = gw.filter((w) => w.error).map((w) => `${shortLabel(w.label || w.address || "?", 6)}: ${w.error}`);
const errLine = errs.length ? `<div class="wcoinerr">⚠ ${esc(errs.join(" · "))}</div>` : ""; const errLine = errs.length ? `<div class="wcoinerr">⚠ ${esc(errs.join(" · "))}</div>` : "";
el.innerHTML = ` el.innerHTML = `
<div class="waddwrap">
<button id="stripAddMore" title="Add another ${esc(meta.coinName)} wallet">+ Add another ${esc(meta.ticker)}</button>
</div>
<div class="wcoinhead"> <div class="wcoinhead">
<button class="wback" id="stripBack" title="Back to coin list">← Back</button> <button class="wback" id="stripBack" title="Back to coin list">← Back</button>
<span class="wctitle">${logoSvg(meta.logo, 16)} ${esc(meta.coinName)}<span class="wcount">· ${gw.length} address${gw.length === 1 ? "" : "es"}</span></span> <span class="wctitle">${logoSvg(meta.logo, 16)} ${esc(meta.coinName)}<span class="wcount">· ${gw.length} address${gw.length === 1 ? "" : "es"}</span></span>
@ -2587,12 +2539,15 @@ function renderInlineCoinList(el, groupKey, group) {
</div> </div>
<div class="wcoinsub"> <div class="wcoinsub">
<span class="wprice">${esc(priceTxt)}<span class="wsep" style="margin-left:4px">/ ${esc(meta.ticker)}</span></span> <span class="wprice">${esc(priceTxt)}<span class="wsep" style="margin-left:4px">/ ${esc(meta.ticker)}</span></span>
<span class="wsep">·</span> ${multiAddr ? `<span class="wsep">·</span>
<span class="wtot">${esc(totalNative)} ${esc(meta.ticker)}</span> <span class="wtot">${esc(totalNative)} ${esc(meta.ticker)}</span>
${totalFiat ? `<span class="wtotfiat">(${esc(totalFiat)})</span>` : ""} ${totalFiat ? `<span class="wtotfiat">(${esc(totalFiat)})</span>` : ""}` : ""}
</div> </div>
${errLine} ${errLine}
${rows}`; ${rows}
<div class="waddwrap">
<button id="stripAddMore" title="Add another ${esc(meta.coinName)} wallet">+ Add another ${esc(meta.ticker)}</button>
</div>`;
const back = () => { stripView = { mode: "coins", groupKey: null }; renderWalletStrip(); }; const back = () => { stripView = { mode: "coins", groupKey: null }; renderWalletStrip(); };
el.querySelector("#stripBack").addEventListener("click", back); el.querySelector("#stripBack").addEventListener("click", back);
el.querySelector("#stripBackX").addEventListener("click", back); el.querySelector("#stripBackX").addEventListener("click", back);
@ -2664,13 +2619,6 @@ function renderInlineCoinList(el, groupKey, group) {
} catch (er) { showErr(cleanErr(er)); } } catch (er) { showErr(cleanErr(er)); }
})); }));
// Per-address asset list toggle. // Per-address asset list toggle.
el.querySelectorAll("[data-lpassets]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
const id = b.dataset.lpassets;
if (expandedAddrAssets.has(id)) expandedAddrAssets.delete(id);
else expandedAddrAssets.add(id);
renderWalletStrip();
}));
el.querySelector("#stripAddMore").addEventListener("click", async () => { el.querySelector("#stripAddMore").addEventListener("click", async () => {
// Add another wallet of the same coin+network directly, without // Add another wallet of the same coin+network directly, without
// opening the picker sheet — the user is already inside this coin's // opening the picker sheet — the user is already inside this coin's
@ -3137,21 +3085,39 @@ function renderTokens() {
card.hidden = tokens.length === 0; card.hidden = tokens.length === 0;
const hintEl = $("tokensHint"); 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 (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; card.hidden = false;
el.innerHTML = tokens.map((t) => { // A symbol claimed by more than one contract is the lookalike pattern:
// spam mints borrow a trusted ticker so a careless send lands on the
// wrong contract. We can't tell which is genuine, so we don't guess —
// we mark every member of the clash and let the user check the address.
const symCount = new Map();
for (const t of tokens) {
const k = (t.symbol || "").toLowerCase();
if (t.known && k) symCount.set(k, (symCount.get(k) || 0) + 1);
}
const nativeUnits = walletBalanceUnits(sel()) || 0;
const nativeRow = `<div class="tx asset native" style="grid-template-columns:1fr auto;cursor:default;align-items:center">
<div><div><b>${esc(s.meta?.ticker || chain().toUpperCase())}</b> <span class="hint">${esc(s.meta?.coinLabel || "")}</span></div>
<div class="hint">native</div></div>
<div class="amt2 in" style="align-self:center;text-align:right">${esc(fmtBig(nativeUnits, s.decimals))}</div>
</div>`;
const cnt = $("tokensCount");
if (cnt) cnt.textContent = `· ${tokens.length + 1}`;
el.innerHTML = nativeRow + tokens.map((t) => {
const dec = Number(t.decimals) || 0; const dec = Number(t.decimals) || 0;
const short = String(t.mint || ""); const short = String(t.mint || "");
const addrLine = `${esc(short.slice(0, 10))}…${esc(short.slice(-6))}`; const addrLine = `${esc(short.slice(0, 10))}…${esc(short.slice(-6))}`;
const clash = t.known && symCount.get((t.symbol || "").toLowerCase()) > 1;
// Unknown contracts have no decimals, so a raw integer would be a // Unknown contracts have no decimals, so a raw integer would be a
// misleading "balance". Say so instead of inventing a number. // misleading "balance". Say so instead of inventing a number.
const title = t.known const title = t.known
? `${esc(t.symbol)}${t.name ? ' <span class="hint">' + esc(t.name) + "</span>" : ""}` ? `${esc(t.symbol)}${t.name ? ' <span class="hint">' + esc(t.name) + "</span>" : ""}${clash ? ' <span class="ttag tdupe" title="More than one contract here uses this symbol — check the address before sending">LOOK-ALIKE</span>' : ""}`
: `<span class="hint">Unknown token</span>`; : `<span class="hint">Unknown token</span>`;
const amount = t.known const amount = t.known
? esc(fmtTokenAmount(t.balance, dec)) ? `<span title="${esc(fmtTokenAmountExact(t.balance, dec))} ${esc(t.symbol)}">${esc(fmtTokenAmount(t.balance, dec))}</span>`
: `<span class="hint mono" title="No registry entry for this contract — raw on-chain amount, decimals unknown">${esc(t.balance)} raw</span>`; : `<span class="hint mono" title="No registry entry for this contract — raw on-chain amount, decimals unknown">${esc(t.balance)} raw</span>`;
return `<div class="tx" style="grid-template-columns:1fr auto;cursor:default;align-items:center"> return `<div class="tx asset" style="grid-template-columns:1fr auto;cursor:default;align-items:center">
<div><div>${title}</div><div class="hint mono">${addrLine}</div></div> <div><div>${title}</div><div class="hint mono" title="${esc(short)}">${addrLine}</div></div>
<div class="amt2 in" style="align-self:center;text-align:right">${amount}</div> <div class="amt2 in" style="align-self:center;text-align:right">${amount}</div>
</div>`; </div>`;
}).join(""); }).join("");
@ -3165,11 +3131,30 @@ function fmtTokenAmount(rawStr, decimals) {
const neg = s.startsWith("-"); const neg = s.startsWith("-");
const abs = neg ? s.slice(1) : s; const abs = neg ? s.slice(1) : s;
const d = Number(decimals) || 0; const d = Number(decimals) || 0;
if (d === 0) return (neg ? "-" : "") + abs; const group = (w) => w.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
if (d === 0) return (neg ? "-" : "") + group(abs);
const pad = abs.padStart(d + 1, "0"); const pad = abs.padStart(d + 1, "0");
const whole = pad.slice(0, pad.length - d); const whole = pad.slice(0, pad.length - d);
let frac = pad.slice(pad.length - d).replace(/0+$/, "");
// An 18-decimal token renders as 60000000.000000005435817984 without a
// cap — 26 digits of noise for a balance whose meaningful part is the
// first few. Keep enough to distinguish small amounts, drop the rest;
// the exact value stays available in the row's title attribute.
const CAP = 8;
let truncated = false;
if (frac.length > CAP) { frac = frac.slice(0, CAP).replace(/0+$/, ""); truncated = true; }
return (neg ? "-" : "") + group(whole) + (frac ? "." + frac : "") + (truncated && !frac ? "…" : "");
}
// Exact, ungrouped rendering for tooltips — what fmtTokenAmount hides.
function fmtTokenAmountExact(rawStr, decimals) {
const s = String(rawStr || "0");
const neg = s.startsWith("-");
const abs = neg ? s.slice(1) : s;
const d = Number(decimals) || 0;
if (d === 0) return (neg ? "-" : "") + abs;
const pad = abs.padStart(d + 1, "0");
const frac = pad.slice(pad.length - d).replace(/0+$/, ""); const frac = pad.slice(pad.length - d).replace(/0+$/, "");
return (neg ? "-" : "") + whole + (frac ? "." + frac : ""); return (neg ? "-" : "") + pad.slice(0, pad.length - d) + (frac ? "." + frac : "");
} }
function applyUnitPicker() { function applyUnitPicker() {
@ -3256,11 +3241,48 @@ function balanceSum(b) {
// ---- history --------------------------------------------------------------- // ---- history ---------------------------------------------------------------
// History asset filter (0.9.2). Both on by default — these narrow a
// combined feed, they don't opt into one.
let histShowNative = true;
let histShowTokens = true;
let histFilterWired = false;
function wireHistFilter() {
if (histFilterWired) return;
const n = $("histNative"), t = $("histTokens");
if (!n || !t) return;
histFilterWired = true;
// Unchecking both would leave an empty list with no hint why, so the
// last one standing stays checked.
const onChange = () => {
if (!n.checked && !t.checked) { n.checked = true; }
histShowNative = n.checked;
histShowTokens = t.checked;
renderHistory();
};
n.addEventListener("change", onChange);
t.addEventListener("change", onChange);
}
function renderHistory() { function renderHistory() {
const s = sel(); const s = sel();
const list = s?.history || []; const all = s?.history || [];
const el = $("txlist"); const el = $("txlist");
if (!list.length) { el.innerHTML = `<div class="empty">${s?.scanning ? "Syncing…" : "No transactions yet."}</div>`; return; } // Only offer the filter when there is actually something to filter.
const hasTokens = all.some((t) => t.asset);
const filterEl = $("histFilter");
if (filterEl) {
filterEl.hidden = !hasTokens;
const nativeLbl = $("histNativeLbl");
if (nativeLbl) nativeLbl.textContent = s?.meta?.ticker || "Coin";
}
const list = hasTokens
? all.filter((t) => (t.asset ? histShowTokens : histShowNative))
: all;
const cntEl = $("histCount");
if (cntEl) cntEl.textContent = hasTokens ? `${list.length} of ${all.length}` : "";
if (!list.length) {
el.innerHTML = `<div class="empty">${s?.scanning ? "Syncing…" : (all.length ? "Nothing matches this filter." : "No transactions yet.")}</div>`;
return;
}
const dec = s?.decimals ?? 8; const dec = s?.decimals ?? 8;
el.innerHTML = list.map((t) => { el.innerHTML = list.map((t) => {
// `delta` arrives in three shapes now: a number (UTXO chains), a decimal // `delta` arrives in three shapes now: a number (UTXO chains), a decimal
@ -3281,9 +3303,14 @@ function renderHistory() {
// string keeps every digit. // string keeps every digit.
const magnitude = known ? String(t.delta).trim().replace(/^[-+]/, "") : ""; const magnitude = known ? String(t.delta).trim().replace(/^[-+]/, "") : "";
const isZero = known && /^0*$/.test(magnitude); const isZero = known && /^0*$/.test(magnitude);
// A token row carries its OWN decimals and ticker — rendering a USDT
// amount against the chain's 6-decimal TRX scale (or an 18-decimal
// ERC-20 against 18 for ETH) would be off by orders of magnitude.
const rowDec = t.asset ? (t.assetDecimals ?? 0) : dec;
const rowTicker = t.asset ? ` ${t.asset}` : "";
const amountHtml = !known ? "—" const amountHtml = !known ? "—"
: isZero ? "—" : isZero ? "—"
: `${inc ? "+" : "−"}${esc(fmtBig(magnitude, dec))}`; : `${inc ? "+" : "−"}${esc(fmtTokenAmount(magnitude, rowDec))}${esc(rowTicker)}`;
const icon = inc === null ? "·" : inc ? "↓" : "↑"; const icon = inc === null ? "·" : inc ? "↓" : "↑";
const iconCls = inc === null ? "" : inc ? "in" : "out"; const iconCls = inc === null ? "" : inc ? "in" : "out";
return `<div class="tx" data-txid="${esc(t.txid)}" title="${esc(t.txid)}"> return `<div class="tx" data-txid="${esc(t.txid)}" title="${esc(t.txid)}">
@ -3295,6 +3322,7 @@ function renderHistory() {
</div>`; </div>`;
}).join(""); }).join("");
el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, row.dataset.txid)))); el.querySelectorAll(".tx").forEach((row) => row.addEventListener("click", () => openUrl(explorerHref(sel().explorerTx, row.dataset.txid))));
wireHistFilter();
} }
function shortAddr(a) { function shortAddr(a) {
if (!a) return ""; if (!a) return "";
@ -3333,6 +3361,27 @@ function drawQr(text) {
// ---- receive actions ------------------------------------------------------- // ---- receive actions -------------------------------------------------------
// 0.9.2: the QR is collapsed by default — it was 200px of permanent
// vertical space above the address, which pushed the asset list off
// screen. The choice sticks per-user because someone who receives by
// QR wants it every time, and someone who copies never wants it.
(function wireQrToggle() {
const btn = $("qrToggle"); if (!btn) return;
let open = false;
try { open = localStorage.getItem("aegis/qrOpen") === "1"; } catch {}
const paint = () => {
const wrap = $("qrWrap"); if (!wrap) return;
wrap.hidden = !open;
btn.setAttribute("aria-expanded", open ? "true" : "false");
btn.classList.toggle("primary", open);
};
btn.addEventListener("click", () => {
open = !open;
try { localStorage.setItem("aegis/qrOpen", open ? "1" : "0"); } catch {}
paint();
});
paint();
})();
$("copyAddr").addEventListener("click", async () => { $("copyAddr").addEventListener("click", async () => {
try { await navigator.clipboard.writeText(sel().address); flash($("copyAddr"), "Copied"); } catch {} try { await navigator.clipboard.writeText(sel().address); flash($("copyAddr"), "Copied"); } catch {}
}); });