From a10f8e9507c686bd0fd48fc5c0c1a030588383ad Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 25 Mar 2026 14:37:35 -0700 Subject: [PATCH] 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 --- .../rwallet/components/folk-wallet-viewer.ts | 55 ++++++++++++++++--- modules/rwallet/mod.ts | 5 +- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts index 1295d11..865b486 100644 --- a/modules/rwallet/components/folk-wallet-viewer.ts +++ b/modules/rwallet/components/folk-wallet-viewer.ts @@ -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 '
No token balances found.
'; 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) => { diff --git a/modules/rwallet/mod.ts b/modules/rwallet/mod.ts index c72b684..b3823e5 100644 --- a/modules/rwallet/mod.ts +++ b/modules/rwallet/mod.ts @@ -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: ``, - scripts: ``, + scripts: ``, styles: ``, }); }