From 2142bc9c61ce801e88ec406a540ef6a8c338648e Mon Sep 17 00:00:00 2001 From: Timidan Date: Sat, 30 May 2026 22:45:27 +0100 Subject: [PATCH 1/5] feat(sim-results): unify asset-change sections as anchored rows Native Token Change and Token Movements (Address + Chronological) now share full-width anchored rows: Phosphor ArrowCircle direction icons, asset chips, addresses left, formatted amounts pinned right. - formatDisplayAmount: round float tails, add thousands separators, keep full value in title - formatMovementAmount: fix chronological view showing raw base-units instead of the human amount - drop the now-dead CSS from both stylesheets --- src/components/ExecutionStackTrace.tsx | 85 +++-- src/components/TokenMovementsPanel.tsx | 239 ++++++-------- .../simulation-results/formatters.ts | 24 ++ src/styles/SimulationResultsPage.css | 60 +--- src/styles/TokenMovementsPanel.css | 297 +++++++----------- src/utils/tokenMovements.ts | 19 ++ 6 files changed, 286 insertions(+), 438 deletions(-) diff --git a/src/components/ExecutionStackTrace.tsx b/src/components/ExecutionStackTrace.tsx index f5aec7fb..afd5ce3a 100644 --- a/src/components/ExecutionStackTrace.tsx +++ b/src/components/ExecutionStackTrace.tsx @@ -11,7 +11,10 @@ import { AccordionItem, AccordionTrigger, } from "./ui/accordion"; -import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./ui/table"; +import { formatDisplayAmount } from "./simulation-results/formatters"; +// Anchored asset-change rows share the .tm-* classes defined in TokenMovementsPanel.css +// (always bundled — this module imports TokenMovementsPanel below). +import { ArrowCircleUpRight, ArrowCircleDownLeft } from "@phosphor-icons/react"; import { extractTokenMovements } from "../utils/tokenMovements"; // Re-export types for backward compatibility @@ -218,51 +221,43 @@ const ExecutionStackTrace: React.FC = (props) => { {orderedAssetChanges.rows.length} -
- - - Address - Asset - Delta Amount - - - - {orderedAssetChanges.rows.map((change: any, idx: number) => { - const direction = normalizeAssetDirection(change); - const isPositive = direction === "in"; - const isNegative = direction === "out"; - const amountClass = isPositive - ? "sim-amount--positive" - : isNegative - ? "sim-amount--negative" - : ""; - const showIncomingDivider = - idx === orderedAssetChanges.outgoingCount && - orderedAssetChanges.outgoingCount > 0 && - orderedAssetChanges.incomingCount > 0; - return ( - - {showIncomingDivider && ( - - )} - - +
+ {orderedAssetChanges.rows.map((change: any, idx: number) => { + const direction = normalizeAssetDirection(change); + const isPositive = direction === "in"; + const isNegative = direction === "out"; + const dirClass = isNegative ? "tm-out" : isPositive ? "tm-in" : "tm-neutral"; + const showIncomingDivider = + idx === orderedAssetChanges.outgoingCount && + orderedAssetChanges.outgoingCount > 0 && + orderedAssetChanges.incomingCount > 0; + const f = formatDisplayAmount(change.amount || change.rawAmount); + return ( + + {showIncomingDivider &&
+ + + \u25cf + {change.symbol || "Unknown"} + + +
+ {f.display} +
+ + + ); + })} + )} diff --git a/src/components/TokenMovementsPanel.tsx b/src/components/TokenMovementsPanel.tsx index a8745d1d..65b203ca 100644 --- a/src/components/TokenMovementsPanel.tsx +++ b/src/components/TokenMovementsPanel.tsx @@ -7,15 +7,17 @@ import { fetchTokenPrices, fetchTokenMetadata, getTokenIconUrl, + formatMovementAmount, type TokenType, type BalanceChange, type TokenMovement, type TokenPrice, } from "../utils/tokenMovements"; import { normalizeValue } from "../utils/displayFormatters"; +import { formatDisplayAmount } from "./simulation-results/formatters"; +import { ArrowCircleUpRight, ArrowCircleDownLeft } from "@phosphor-icons/react"; import { ZERO_ADDRESS } from "../utils/addressConstants"; import { Button } from "./ui/button"; -import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./ui/table"; import "../styles/TokenMovementsPanel.css"; interface TokenMovementsPanelProps { @@ -305,95 +307,54 @@ const TokenMovementsPanel: React.FC = ({ {/* Balance changes table - different columns for different token types */} {groupingMode === "address" && currentChanges.length > 0 && (
- - - - Address - Asset - {/* ERC-721 and ERC-1155 have Token ID column, ERC-20 has Value column */} - {(currentTab === "ERC-721" || currentTab === "ERC-1155") ? ( - <> - Token ID - Balance Change - - ) : ( - <> - Balance Change - Value - - )} - - - - {currentChanges.map((change, idx) => { - const prevChange = idx > 0 ? currentChanges[idx - 1] : null; - const showDivider = prevChange && prevChange.rawDelta < 0n && change.rawDelta >= 0n; - const colCount = (currentTab === "ERC-721" || currentTab === "ERC-1155") ? 4 : 4; - return ( - - {showDivider && ( - - )} - - - ); - })} - -
+
+ {currentChanges.map((change, idx) => { + const prevChange = idx > 0 ? currentChanges[idx - 1] : null; + const showDivider = prevChange && prevChange.rawDelta < 0n && change.rawDelta >= 0n; + return ( + + {showDivider &&
)} {groupingMode === "chronological" && currentMovements.length > 0 && (
- - - - From - To - Amount - Asset - Asset Type - Token ID - - - - {currentMovements.map((movement, idx) => { - const senderLower = senderAddress?.toLowerCase(); - const prevMovement = idx > 0 ? currentMovements[idx - 1] : null; - const showDivider = senderLower && prevMovement && - prevMovement.from.toLowerCase() === senderLower && - movement.from.toLowerCase() !== senderLower; - return ( - - {showDivider && ( - - )} - - - ); - })} - -
+
+ {currentMovements.map((movement, idx) => { + const senderLower = senderAddress?.toLowerCase(); + const prevMovement = idx > 0 ? currentMovements[idx - 1] : null; + const showDivider = senderLower && prevMovement && + prevMovement.from.toLowerCase() === senderLower && + movement.from.toLowerCase() !== senderLower; + return ( + + {showDivider &&
)} @@ -420,7 +381,6 @@ const TokenMovementRow: React.FC = ({ isNft = false, }) => { const isNegative = change.rawDelta < 0n; - const deltaClass = isNegative ? "delta-negative" : "delta-positive"; const [iconError, setIconError] = useState(false); // Check if we have a real symbol (not a truncated address) @@ -466,58 +426,44 @@ const TokenMovementRow: React.FC = ({ return `$${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; + const delta = formatDisplayAmount(change.delta); + return ( - - - - {isNegative ? "↗" : "↘"} - - {renderHighlightable(change.address, formatAddress(change.address), "address-value")} +
+
+ {isNegative ? ( +
+
+ {delta.display} + + {isNft ? (change.tokenId ? `#${change.tokenId}` : "—") : formatUsd(usdValue)} + +
+
); }; @@ -574,27 +520,32 @@ const TokenMovementChronologicalRow: React.FC - - {renderHighlightable(movement.from, formatParty(movement.from, "from"), "address-value")} - {fromLabel && [{fromLabel}]} - - - {renderHighlightable(movement.to, formatParty(movement.to, "to"), "address-value")} - {toLabel && [{toLabel}]} - - - {movement.amount} - - - {renderHighlightable(movement.tokenAddress, displaySymbol, "token-symbol")} - - {movement.tokenType} - - {movement.tokenId ? `#${movement.tokenId}` : "—"} - -
+
+
+ + + {renderHighlightable(movement.tokenAddress, displaySymbol, "token-symbol")} + + + {renderHighlightable(movement.from, formatParty(movement.from, "from"), "tm-addr")} + {fromLabel && [{fromLabel}]} + + {renderHighlightable(movement.to, formatParty(movement.to, "to"), "tm-addr")} + {toLabel && [{toLabel}]} + +
+
+ + {amount.display.replace(/^\+/, "")} + + {movement.tokenId ? `#${movement.tokenId}` : movement.tokenType} +
+
); }; diff --git a/src/components/simulation-results/formatters.ts b/src/components/simulation-results/formatters.ts index b5c2da01..f140a5a9 100644 --- a/src/components/simulation-results/formatters.ts +++ b/src/components/simulation-results/formatters.ts @@ -90,6 +90,30 @@ export const formatEth = (weiValue?: string | null) => { } }; +/** + * Format an already-decimal amount string for compact display while keeping the + * full value available (e.g. for a `title` tooltip). Turns the raw float tails + * like "-0.08999999999999997" into a readable "−0.0900". + */ +export const formatDisplayAmount = ( + value?: string | null +): { display: string; full: string } => { + const full = String(value ?? "").trim(); + if (!full) return { display: "—", full: "" }; + const n = Number(full); + if (!Number.isFinite(n) || n === 0) { + return { display: n === 0 ? "0" : full, full }; + } + const sign = n > 0 ? "+" : "−"; // U+2212 minus + const abs = Math.abs(n); + const absStr = Number.isInteger(abs) + ? abs.toLocaleString("en-US") // whole counts get thousands separators: 261,000 + : abs >= 0.001 + ? abs.toLocaleString("en-US", { minimumFractionDigits: 4, maximumFractionDigits: 4 }) + : Number(abs.toPrecision(3)).toString(); + return { display: `${sign}${absStr}`, full }; +}; + export const calculateIntrinsicGas = (calldata?: string | null): number => { const INTRINSIC_BASE = 21000; if (!calldata || calldata === "0x") return INTRINSIC_BASE; diff --git a/src/styles/SimulationResultsPage.css b/src/styles/SimulationResultsPage.css index 989934a9..85b4ece5 100644 --- a/src/styles/SimulationResultsPage.css +++ b/src/styles/SimulationResultsPage.css @@ -526,65 +526,7 @@ body:has(.sim-results-page) > #root { RETURN DATA (multi-line) ============================================ */ -/* ============================================ - BALANCE CHANGES TABLE (Flat Design) - ============================================ */ - -.sim-balance-changes__table { - width: 100%; - border-collapse: collapse; - font-size: 14px; - font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", monospace; -} - -.sim-balance-changes__table thead tr { - border-bottom: 1px solid var(--sim-border); -} - -.sim-balance-changes__table th { - padding: 10px 14px; - text-align: left; - color: var(--sim-text-muted); - font-weight: 600; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.sim-balance-changes__table th.text-right { - text-align: right; -} - -.sim-balance-changes__table td { - padding: 10px 14px; - color: var(--sim-text); - border-bottom: 1px solid var(--sim-border); -} - -.sim-balance-changes__table td.text-right { - text-align: right; -} - -.sim-balance-changes__table td.sim-address { - color: #22d3ee; -} - -.sim-balance-changes__table tr.sim-balance-changes__group-divider td { - padding: 0; - border-bottom: none; - height: 0; - border-top: 2px solid rgba(255, 255, 255, 0.2); -} - -.sim-balance-changes__table .sim-amount--positive { - color: var(--sim-success); - font-weight: 500; -} - -.sim-balance-changes__table .sim-amount--negative { - color: var(--sim-error); - font-weight: 500; -} +/* Native Token Change now uses the shared .tm-* anchored rows (TokenMovementsPanel.css) */ /* ============================================ SECTION HEADERS (Flat Design) diff --git a/src/styles/TokenMovementsPanel.css b/src/styles/TokenMovementsPanel.css index 48d2ad55..17b0939c 100644 --- a/src/styles/TokenMovementsPanel.css +++ b/src/styles/TokenMovementsPanel.css @@ -92,141 +92,142 @@ overflow-x: auto; } -/* Direction group divider (between outgoing and incoming rows) */ -.token-movements-table tr.token-movements-group-divider td { - padding: 0; - border-bottom: none; +/* ============================================ + Anchored rows (Address view) — full-width + ledger rows: direction pill + address + asset + chip on the left; delta over USD pinned right. + ============================================ */ +.tm-list { + display: flex; + flex-direction: column; +} +.tm-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + min-height: 52px; + padding: 10px 4px; +} +.tm-row + .tm-row { + border-top: 1px solid rgba(255, 255, 255, 0.06); +} +.tm-row:hover { + background: rgba(255, 255, 255, 0.022); +} +.tm-divider { height: 0; border-top: 2px solid rgba(255, 255, 255, 0.12); + margin: 2px 0; } -/* Table styles */ -.token-movements-table { - width: 100%; - border-collapse: collapse; - font-size: 12px; - table-layout: fixed; +.tm-left { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; } - -.token-movements-table thead th { - text-align: left; - padding: 4px 10px; - color: #666; - font-weight: 500; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.3px; - border-bottom: 1px solid rgba(255, 255, 255, 0.04); - background: rgba(15, 15, 20, 0.3); +/* Direction glyph: Phosphor ArrowCircle (fill) — self-contained, no extra pill */ +.tm-dir { + flex: none; + display: block; } - -/* Address-grouped table widths */ -.token-movements-table--address th:nth-child(1), -.token-movements-table--address td:nth-child(1) { - width: 22%; -} - -.token-movements-table--address th:nth-child(2), -.token-movements-table--address td:nth-child(2) { - width: 18%; +.tm-out .tm-dir { + color: #ef4444; } - -.token-movements-table--address th:nth-child(3), -.token-movements-table--address td:nth-child(3) { - width: 38%; - text-align: right; +.tm-in .tm-dir { + color: #22c55e; } - -.token-movements-table--address th:nth-child(4), -.token-movements-table--address td:nth-child(4) { - width: 22%; - text-align: right; +.tm-addr { + color: #22d3ee; + font-family: "Fira Code", "Consolas", monospace; + font-size: 13px; } - -/* Chronological table widths */ -.token-movements-table--chronological th:nth-child(1), -.token-movements-table--chronological td:nth-child(1) { - width: 18%; +.tm-asset { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 3px 9px 3px 5px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + background: rgba(255, 255, 255, 0.02); } - -.token-movements-table--chronological th:nth-child(2), -.token-movements-table--chronological td:nth-child(2) { - width: 18%; +.tm-asset-logo { + width: 16px; + height: 16px; + border-radius: 50%; + object-fit: cover; + background: rgba(255, 255, 255, 0.1); + flex-shrink: 0; } - -.token-movements-table--chronological th:nth-child(3), -.token-movements-table--chronological td:nth-child(3) { - width: 11%; - text-align: right; +.tm-asset-logo--fallback { + display: inline-flex; + align-items: center; + justify-content: center; + color: #6b7280; + background: transparent; + font-size: 13px; } -.token-movements-table--chronological th:nth-child(4), -.token-movements-table--chronological td:nth-child(4) { - width: 15%; +.tm-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + flex: none; } - -.token-movements-table--chronological th:nth-child(5), -.token-movements-table--chronological td:nth-child(5) { - width: 11%; +.tm-delta { + font-family: "Fira Code", "Consolas", monospace; + font-size: 16px; + line-height: 1.1; + font-variant-numeric: tabular-nums; } - -.token-movements-table--chronological th:nth-child(6), -.token-movements-table--chronological td:nth-child(6) { - width: 10%; +.tm-usd { + font-family: "Fira Code", "Consolas", monospace; + font-size: 12px; + line-height: 1.1; + font-variant-numeric: tabular-nums; + color: #6b7280; } - -.token-movements-table--chronological th:nth-child(7), -.token-movements-table--chronological td:nth-child(7) { - width: 17%; +.tm-out .tm-delta { + color: #ef4444; } - -.token-movements-table tbody tr { - border-bottom: 1px solid rgba(255, 255, 255, 0.03); - transition: background 0.1s ease; +.tm-in .tm-delta { + color: #22c55e; } - -.token-movements-table tbody tr:hover { - background: rgba(255, 255, 255, 0.02); +.tm-out .tm-usd, +.tm-in .tm-usd { + opacity: 0.85; } - -.token-movements-table tbody tr:last-child { - border-bottom: none; +.tm-out .tm-usd { + color: #ef4444; } - -.token-movements-table td { - padding: 4px 10px; - vertical-align: middle; +.tm-in .tm-usd { + color: #22c55e; } -/* Address cell */ -.address-cell { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +/* Chronological rows: asset chip + "from → to" flow on the left */ +.tm-row--chrono .tm-left { + flex-wrap: wrap; } - -.address-icon { - width: 14px; - height: 14px; +.tm-flow { display: inline-flex; align-items: center; - justify-content: center; - border-radius: 3px; - font-size: 10px; - margin-right: 6px; - vertical-align: middle; + gap: 7px; + min-width: 0; } - -.address-icon.icon-out { - background: rgba(239, 68, 68, 0.15); - color: #ef4444; +.tm-flow .tm-addr { + font-size: 12px; } - -.address-icon.icon-in { - background: rgba(34, 197, 94, 0.15); - color: #22c55e; +.tm-arrow { + color: #6b7280; + font-size: 13px; +} +.tm-neutral .tm-delta { + color: #d4d4d4; } +/* address-value is still emitted by resultFormatter.ts */ .address-value { color: #d4d4d4; font-family: "Fira Code", "Consolas", monospace; @@ -242,34 +243,6 @@ vertical-align: middle; } -/* Token cell */ -.token-cell { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.token-cell-content { - display: inline-flex; - align-items: center; - gap: 6px; -} - -.token-icon-img { - width: 16px; - height: 16px; - border-radius: 50%; - object-fit: cover; - background: rgba(255, 255, 255, 0.1); - flex-shrink: 0; -} - -.token-icon-fallback { - color: #6b7280; - font-size: 15px; - flex-shrink: 0; -} - .token-symbol { color: #e2e8f0; font-weight: 500; @@ -282,53 +255,6 @@ font-size: 11px; } -/* Token ID cell (for NFTs) */ -.token-id-cell { - font-family: "Fira Code", "Consolas", monospace; - font-size: 11px; - color: #d4d4d4; - text-align: left; - white-space: nowrap; -} - -/* Delta cell */ -.delta-cell { - font-family: "Fira Code", "Consolas", monospace; - font-weight: 500; - font-size: 12px; - text-align: right; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.delta-cell.delta-negative { - color: #ef4444; -} - -.delta-cell.delta-positive { - color: #22c55e; -} - -/* USD value cell */ -.usd-cell { - font-family: "Fira Code", "Consolas", monospace; - font-size: 11px; - text-align: right; - white-space: nowrap; - color: #6b7280; -} - -.usd-cell.delta-negative { - color: #ef4444; -} - -.usd-cell.delta-positive { - color: #22c55e; -} - -/* Empty state */ - /* ====================================== Cross-reference highlighting ====================================== */ @@ -359,13 +285,4 @@ gap: 12px; align-items: flex-start; } - - .token-movements-table { - font-size: 13px; - } - - .token-movements-table td, - .token-movements-table th { - padding: 8px 12px; - } } diff --git a/src/utils/tokenMovements.ts b/src/utils/tokenMovements.ts index 6e36db19..601db377 100644 --- a/src/utils/tokenMovements.ts +++ b/src/utils/tokenMovements.ts @@ -371,6 +371,25 @@ export function getCachedTokenMetadata(tokenAddress: string): TokenMetadata | nu return tokenMetadataCache.get(tokenAddress.toLowerCase()) ?? null; } +/** + * Format a movement's raw base-unit `amount` into a human-readable decimal + * string, mirroring aggregateBalanceChanges: ERC-20 divides by cached decimals + * (default 18); NFTs are whole counts. Returns an unsigned string. + */ +export function formatMovementAmount(movement: TokenMovement): string { + if (movement.formattedAmount) return movement.formattedAmount; + const raw = String(movement.amount ?? "0"); + const isNft = movement.tokenType === "ERC-721" || movement.tokenType === "ERC-1155"; + if (isNft) return raw; + try { + const decimals = + movement.decimals ?? getCachedTokenMetadata(movement.tokenAddress)?.decimals ?? 18; + return ethers.utils.formatUnits(BigInt(raw === "undefined" || raw === "" ? "0" : raw), decimals); + } catch { + return raw; + } +} + // Pre-cache common tokens (Ethereum Mainnet) setTokenMetadataCache("0xdAC17F958D2ee523a2206206994597C13D831ec7", { symbol: "USDT", name: "Tether USD", decimals: 6 }); setTokenMetadataCache("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", { symbol: "USDC", name: "USD Coin", decimals: 6 }); From 32daf20286f9b687d997cfd299b91628fd68c973 Mon Sep 17 00:00:00 2001 From: Timidan Date: Sun, 31 May 2026 11:12:37 +0100 Subject: [PATCH 2/5] fix(sim-results): harden amount formatting + asset-row review fixes - formatDisplayAmount: parse the decimal string directly instead of via Number(), preserving precision on large amounts and avoiding scientific notation on tiny ones - formatMovementAmount: use cached decimals only, matching aggregateBalanceChanges (movement.decimals is never populated) - stable row keys so a row's icon-error state can't stick to the wrong token after tab/filter changes - NFT chronological row keeps asset type alongside the token id - aria-hidden on decorative fallback bullets --- src/components/ExecutionStackTrace.tsx | 2 +- src/components/TokenMovementsPanel.tsx | 8 ++-- .../simulation-results/formatters.ts | 48 ++++++++++++++----- src/utils/tokenMovements.ts | 4 +- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/components/ExecutionStackTrace.tsx b/src/components/ExecutionStackTrace.tsx index afd5ce3a..75fdeb3a 100644 --- a/src/components/ExecutionStackTrace.tsx +++ b/src/components/ExecutionStackTrace.tsx @@ -246,7 +246,7 @@ const ExecutionStackTrace: React.FC = (props) => { {change.address ? `${change.address.slice(0, 10)}\u2026${change.address.slice(-8)}` : "\u2014"} - \u25cf + {change.symbol || "Unknown"}
diff --git a/src/components/TokenMovementsPanel.tsx b/src/components/TokenMovementsPanel.tsx index 65b203ca..313acb5e 100644 --- a/src/components/TokenMovementsPanel.tsx +++ b/src/components/TokenMovementsPanel.tsx @@ -312,7 +312,7 @@ const TokenMovementsPanel: React.FC = ({ const prevChange = idx > 0 ? currentChanges[idx - 1] : null; const showDivider = prevChange && prevChange.rawDelta < 0n && change.rawDelta >= 0n; return ( - + {showDivider && ); diff --git a/src/components/simulation-results/formatters.ts b/src/components/simulation-results/formatters.ts index f140a5a9..fcbe3bb7 100644 --- a/src/components/simulation-results/formatters.ts +++ b/src/components/simulation-results/formatters.ts @@ -100,18 +100,44 @@ export const formatDisplayAmount = ( ): { display: string; full: string } => { const full = String(value ?? "").trim(); if (!full) return { display: "—", full: "" }; - const n = Number(full); - if (!Number.isFinite(n) || n === 0) { - return { display: n === 0 ? "0" : full, full }; + + // Parse the decimal string directly. Going through Number() loses precision on + // large token amounts (9007199254740993 → …992) and yields scientific notation + // for tiny ones — both showed up in review. + const m = full.match(/^([+-]?)(\d+)(?:\.(\d+))?$/); + if (!m) return { display: full, full }; + + const intRaw = m[2].replace(/^0+(?=\d)/, ""); + const fracTrimmed = (m[3] ?? "").replace(/0+$/, ""); + const intIsZero = /^0+$/.test(intRaw); + if (intIsZero && !fracTrimmed) return { display: "0", full }; + + const sign = m[1] === "-" ? "−" : "+"; // U+2212 minus / + for positive + let body: string; + + if (!intIsZero) { + // Magnitude ≥ 1. + if (intRaw.length <= 15) { + // Safe for Number — round to 4dp then regroup. + const r = Math.abs(Number(full)).toFixed(4).replace(/\.?0+$/, ""); + const [ip, fp] = r.split("."); + body = ip.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (fp ? `.${fp}` : ""); + } else { + // Too large for Number — group the integer as a string, truncate the fraction. + const frac4 = fracTrimmed.slice(0, 4).replace(/0+$/, ""); + const grouped = intRaw.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + body = frac4 ? `${grouped}.${frac4}` : grouped; + } + } else { + // Magnitude < 1 — never scientific notation. + const firstSig = fracTrimmed.search(/[1-9]/); + body = + firstSig <= 2 + ? Math.abs(Number(full)).toFixed(4) // 0.0900, 0.0010 (≥ 0.001) + : `0.${fracTrimmed.slice(0, firstSig + 3)}`; // 0.000108 (< 0.001, ~3 sig figs) } - const sign = n > 0 ? "+" : "−"; // U+2212 minus - const abs = Math.abs(n); - const absStr = Number.isInteger(abs) - ? abs.toLocaleString("en-US") // whole counts get thousands separators: 261,000 - : abs >= 0.001 - ? abs.toLocaleString("en-US", { minimumFractionDigits: 4, maximumFractionDigits: 4 }) - : Number(abs.toPrecision(3)).toString(); - return { display: `${sign}${absStr}`, full }; + + return { display: `${sign}${body}`, full }; }; export const calculateIntrinsicGas = (calldata?: string | null): number => { diff --git a/src/utils/tokenMovements.ts b/src/utils/tokenMovements.ts index 601db377..e0219c9e 100644 --- a/src/utils/tokenMovements.ts +++ b/src/utils/tokenMovements.ts @@ -382,8 +382,8 @@ export function formatMovementAmount(movement: TokenMovement): string { const isNft = movement.tokenType === "ERC-721" || movement.tokenType === "ERC-1155"; if (isNft) return raw; try { - const decimals = - movement.decimals ?? getCachedTokenMetadata(movement.tokenAddress)?.decimals ?? 18; + // Match aggregateBalanceChanges exactly: cached decimals ?? 18 (it ignores movement.decimals). + const decimals = getCachedTokenMetadata(movement.tokenAddress)?.decimals ?? 18; return ethers.utils.formatUnits(BigInt(raw === "undefined" || raw === "" ? "0" : raw), decimals); } catch { return raw; From a43780681fbeb0c1708b6c1d2917c33453af3f3e Mon Sep 17 00:00:00 2001 From: Timidan Date: Sun, 31 May 2026 11:26:42 +0100 Subject: [PATCH 3/5] refactor(sim-results): a11y + shared styles for asset-change rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - extract shared .tm-* / .token-symbol into styles/asset-rows.css, imported by both TokenMovementsPanel and ExecutionStackTrace — removes the cross-component CSS coupling - role=list/listitem + sr-only direction labels (Outgoing/Incoming/Transfer) so non-visual users get row semantics and direction without relying on color - keyboard-focusable highlightable values (tabIndex + onFocus/onBlur mirroring hover) - ellipsis truncation for long token names --- src/components/ExecutionStackTrace.tsx | 6 +- src/components/TokenMovementsPanel.tsx | 17 ++- src/styles/TokenMovementsPanel.css | 142 +------------------------ src/styles/asset-rows.css | 133 +++++++++++++++++++++++ 4 files changed, 153 insertions(+), 145 deletions(-) create mode 100644 src/styles/asset-rows.css diff --git a/src/components/ExecutionStackTrace.tsx b/src/components/ExecutionStackTrace.tsx index 75fdeb3a..6ddb4bfd 100644 --- a/src/components/ExecutionStackTrace.tsx +++ b/src/components/ExecutionStackTrace.tsx @@ -1,5 +1,6 @@ import React from "react"; import "../styles/ExecutionStackTrace.css"; +import "../styles/asset-rows.css"; import TokenMovementsPanel from "./TokenMovementsPanel"; import { CopyButton } from "./ui/copy-button"; import { Button } from "./ui/button"; @@ -221,7 +222,7 @@ const ExecutionStackTrace: React.FC = (props) => { {orderedAssetChanges.rows.length} -
+
{orderedAssetChanges.rows.map((change: any, idx: number) => { const direction = normalizeAssetDirection(change); const isPositive = direction === "in"; @@ -235,8 +236,9 @@ const ExecutionStackTrace: React.FC = (props) => { return ( {showIncomingDivider &&