fix(rwallet): capture ETHEREUM_TRANSACTION inflows, filter airdrop spam tokens

Transfers: handle ETHEREUM_TRANSACTION txType as inflows (was only scanning
tx.transfers), exclude self-transfers from embedded transfers loop.
Balances: hide unpriced ERC-20s (airdrop spam) while keeping native tokens,
CRDT tokens, and CoinGecko-priced tokens. Filter zero balances on single-chain
Safe endpoint. Bump JS cache to v=20.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-25 14:37:35 -07:00
parent c7c6b6a13b
commit a10f8e9507
2 changed files with 51 additions and 9 deletions

View File

@ -800,13 +800,29 @@ class FolkWalletViewer extends HTMLElement {
const incoming: any[] = [];
const outgoing: any[] = [];
const results = data.results || [];
const addr = this.address.toLowerCase();
for (const tx of results) {
// Outgoing: Safe-initiated multisig transactions
if (tx.txType === "MULTISIG_TRANSACTION") {
outgoing.push(tx);
}
// Incoming: external ETH/token transfers to Safe
if (tx.txType === "ETHEREUM_TRANSACTION") {
if (tx.from && tx.value && tx.value !== "0") {
incoming.push({
type: "ETHER_TRANSFER",
from: tx.from,
to: this.address,
value: tx.value,
executionDate: tx.executionDate,
blockTimestamp: tx.executionDate,
});
}
}
// Embedded transfers (both tx types may have these)
if (tx.transfers) {
for (const t of tx.transfers) {
if (t.to?.toLowerCase() === this.address.toLowerCase()) {
if (t.to?.toLowerCase() === addr && t.from?.toLowerCase() !== addr) {
incoming.push(t);
}
}
@ -2606,7 +2622,13 @@ class FolkWalletViewer extends HTMLElement {
}
const sorted = allBals
.filter(b => parseFloat(b.fiatBalance || "0") > 0.01 || BigInt(b.balance || "0") > 0n)
.filter(b => {
const fiat = parseFloat(b.fiatBalance || "0");
if (fiat > 0.01) return true;
if (b.chainId === "local" || b.tokenAddress?.startsWith("crdt:")) return true;
if (!b.tokenAddress && BigInt(b.balance || "0") > 0n) return true;
return false;
})
.sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"));
const totalUSD = sorted.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);
@ -2679,7 +2701,13 @@ class FolkWalletViewer extends HTMLElement {
}
const sorted = allBals
.filter(b => parseFloat(b.fiatBalance || "0") > 0.01 || BigInt(b.balance || "0") > 0n)
.filter(b => {
const fiat = parseFloat(b.fiatBalance || "0");
if (fiat > 0.01) return true;
if (b.chainId === "local" || b.tokenAddress?.startsWith("crdt:")) return true;
if (!b.tokenAddress && BigInt(b.balance || "0") > 0n) return true;
return false;
})
.sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"));
const totalUSD = sorted.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);
@ -2732,8 +2760,9 @@ class FolkWalletViewer extends HTMLElement {
for (const ch of chains) {
totalChains.add(ch.chainId);
for (const b of ch.balances) {
if (parseFloat(b.fiatBalance || "0") > 0.01 || BigInt(b.balance || "0") > 0n) {
grandTotal += parseFloat(b.fiatBalance || "0");
const fiat = parseFloat(b.fiatBalance || "0");
if (fiat > 0.01 || (!b.tokenAddress && BigInt(b.balance || "0") > 0n)) {
grandTotal += fiat;
totalTokens++;
}
}
@ -3047,7 +3076,13 @@ class FolkWalletViewer extends HTMLElement {
if (unified.length === 0) return '<div class="empty">No token balances found.</div>';
const sorted = unified
.filter((b) => parseFloat(b.fiatBalance || "0") > 0.01 || BigInt(b.balance || "0") > 0n)
.filter((b) => {
const fiat = parseFloat(b.fiatBalance || "0");
if (fiat > 0.01) return true;
if (b.chainId === "local" || b.tokenAddress?.startsWith("crdt:")) return true;
if (!b.tokenAddress && BigInt(b.balance || "0") > 0n) return true;
return false;
})
.sort((a, b) => {
const fiatDiff = parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0");
if (fiatDiff !== 0) return fiatDiff;
@ -3253,7 +3288,13 @@ class FolkWalletViewer extends HTMLElement {
// Aggregate stats across ALL chains (ignoring filter)
const allBalances = this.getUnifiedBalances(true);
const totalUSD = allBalances.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);
const totalTokens = allBalances.filter((b) => parseFloat(b.fiatBalance || "0") > 0 || BigInt(b.balance || "0") > 0n).length;
const totalTokens = allBalances.filter((b) => {
const fiat = parseFloat(b.fiatBalance || "0");
if (fiat > 0.01) return true;
if (b.chainId === "local" || b.tokenAddress?.startsWith("crdt:")) return true;
if (!b.tokenAddress && BigInt(b.balance || "0") > 0n) return true;
return false;
}).length;
// Build chain buttons with "All" filter
const chainButtons = this.detectedChains.map((ch) => {

View File

@ -47,7 +47,8 @@ routes.get("/api/safe/:chainId/:address/balances", async (c) => {
fiatBalance: item.fiatBalance || "0",
fiatConversion: item.fiatConversion || "0",
}));
const enriched = await enrichWithPrices(data, chainId);
const enriched = (await enrichWithPrices(data, chainId))
.filter(b => BigInt(b.balance || "0") > 0n);
c.header("Cache-Control", "public, max-age=30");
return c.json(enriched);
});
@ -1258,7 +1259,7 @@ function renderWallet(spaceSlug: string, initialView?: string) {
modules: getModuleInfoList(),
theme: "dark",
body: `<folk-wallet-viewer space="${spaceSlug}"${viewAttr}></folk-wallet-viewer>`,
scripts: `<script type="module" src="/modules/rwallet/folk-wallet-viewer.js?v=19"></script>`,
scripts: `<script type="module" src="/modules/rwallet/folk-wallet-viewer.js?v=20"></script>`,
styles: `<link rel="stylesheet" href="/modules/rwallet/wallet.css">`,
});
}