From b2c947bfb7edf779b0e3fac622ffb477a6f8ccde Mon Sep 17 00:00:00 2001 From: clawbot Date: Fri, 27 Feb 2026 12:05:08 -0800 Subject: [PATCH 1/4] Extract decodeCalldata to shared module Move the calldata decoding logic (ERC-20 and Uniswap) from approval.js into src/shared/decodeCalldata.js so it can be reused by both the approval screen and transaction history. --- src/shared/decodeCalldata.js | 103 +++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/shared/decodeCalldata.js diff --git a/src/shared/decodeCalldata.js b/src/shared/decodeCalldata.js new file mode 100644 index 0000000..e93c058 --- /dev/null +++ b/src/shared/decodeCalldata.js @@ -0,0 +1,103 @@ +// Decode transaction calldata into human-readable details. +// Shared between the approval screen and transaction history views. +// Returns { name, description, details } or null. + +const { Interface, formatUnits } = require("ethers"); +const { ERC20_ABI } = require("./constants"); +const { TOKEN_BY_ADDRESS } = require("./tokenList"); +const uniswap = require("./uniswap"); + +const erc20Iface = new Interface(ERC20_ABI); + +function formatTxValue(val) { + const parts = val.split("."); + if (parts.length === 1) return val + ".0000"; + const dec = (parts[1] + "0000").slice(0, 4); + return parts[0] + "." + dec; +} + +function decodeCalldata(data, toAddress) { + if (!data || data === "0x" || data.length < 10) return null; + + // Try ERC-20 (approve / transfer) + try { + const parsed = erc20Iface.parseTransaction({ data }); + if (parsed) { + const token = TOKEN_BY_ADDRESS.get(toAddress.toLowerCase()); + const tokenSymbol = token ? token.symbol : null; + const tokenDecimals = token ? token.decimals : 18; + const contractLabel = tokenSymbol + ? tokenSymbol + " (" + toAddress + ")" + : toAddress; + + if (parsed.name === "approve") { + const spender = parsed.args[0]; + const rawAmount = parsed.args[1]; + const maxUint = BigInt( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ); + const isUnlimited = rawAmount === maxUint; + const amountStr = isUnlimited + ? "Unlimited" + : formatTxValue(formatUnits(rawAmount, tokenDecimals)) + + (tokenSymbol ? " " + tokenSymbol : ""); + + return { + name: "Token Approval", + description: tokenSymbol + ? "Approve spending of your " + tokenSymbol + : "Approve spending of an ERC-20 token", + details: [ + { + label: "Token", + value: contractLabel, + address: toAddress, + isToken: true, + }, + { + label: "Spender", + value: spender, + address: spender, + }, + { label: "Amount", value: amountStr }, + ], + }; + } + + if (parsed.name === "transfer") { + const to = parsed.args[0]; + const rawAmount = parsed.args[1]; + const amountStr = + formatTxValue(formatUnits(rawAmount, tokenDecimals)) + + (tokenSymbol ? " " + tokenSymbol : ""); + + return { + name: "Token Transfer", + description: tokenSymbol + ? "Transfer " + tokenSymbol + : "Transfer ERC-20 token", + details: [ + { + label: "Token", + value: contractLabel, + address: toAddress, + isToken: true, + }, + { label: "Recipient", value: to, address: to }, + { label: "Amount", value: amountStr }, + ], + }; + } + } + } catch { + // Not ERC-20 — fall through + } + + // Try Uniswap Universal Router + const routerResult = uniswap.decode(data, toAddress); + if (routerResult) return routerResult; + + return null; +} + +module.exports = { decodeCalldata }; -- 2.49.1 From 93aecc87efab7c26bd135ed289dca9a81e9bd2cb Mon Sep 17 00:00:00 2001 From: clawbot Date: Fri, 27 Feb 2026 12:05:11 -0800 Subject: [PATCH 2/4] Use shared decodeCalldata in approval view Replace the local decodeCalldata function in approval.js with an import from the new shared module. --- src/popup/views/approval.js | 87 +------------------------------------ 1 file changed, 2 insertions(+), 85 deletions(-) diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js index 359b506..7bce818 100644 --- a/src/popup/views/approval.js +++ b/src/popup/views/approval.js @@ -3,6 +3,7 @@ const { state, saveState } = require("../../shared/state"); const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers"); const { ERC20_ABI } = require("../../shared/constants"); const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); +const { decodeCalldata } = require("../../shared/decodeCalldata"); const txStatus = require("./txStatus"); const uniswap = require("../../shared/uniswap"); @@ -41,91 +42,7 @@ function etherscanTokenLink(address) { return `https://etherscan.io/token/${address}`; } -// Try to decode calldata using known ABIs. -// Returns { name, description, details } or null. -function decodeCalldata(data, toAddress) { - if (!data || data === "0x" || data.length < 10) return null; - - // Try ERC-20 (approve / transfer) - try { - const parsed = erc20Iface.parseTransaction({ data }); - if (parsed) { - const token = TOKEN_BY_ADDRESS.get(toAddress.toLowerCase()); - const tokenSymbol = token ? token.symbol : null; - const tokenDecimals = token ? token.decimals : 18; - const contractLabel = tokenSymbol - ? tokenSymbol + " (" + toAddress + ")" - : toAddress; - - if (parsed.name === "approve") { - const spender = parsed.args[0]; - const rawAmount = parsed.args[1]; - const maxUint = BigInt( - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - ); - const isUnlimited = rawAmount === maxUint; - const amountStr = isUnlimited - ? "Unlimited" - : formatTxValue(formatUnits(rawAmount, tokenDecimals)) + - (tokenSymbol ? " " + tokenSymbol : ""); - - return { - name: "Token Approval", - description: tokenSymbol - ? "Approve spending of your " + tokenSymbol - : "Approve spending of an ERC-20 token", - details: [ - { - label: "Token", - value: contractLabel, - address: toAddress, - isToken: true, - }, - { - label: "Spender", - value: spender, - address: spender, - }, - { label: "Amount", value: amountStr }, - ], - }; - } - - if (parsed.name === "transfer") { - const to = parsed.args[0]; - const rawAmount = parsed.args[1]; - const amountStr = - formatTxValue(formatUnits(rawAmount, tokenDecimals)) + - (tokenSymbol ? " " + tokenSymbol : ""); - - return { - name: "Token Transfer", - description: tokenSymbol - ? "Transfer " + tokenSymbol - : "Transfer ERC-20 token", - details: [ - { - label: "Token", - value: contractLabel, - address: toAddress, - isToken: true, - }, - { label: "Recipient", value: to, address: to }, - { label: "Amount", value: amountStr }, - ], - }; - } - } - } catch { - // Not ERC-20 — fall through - } - - // Try Uniswap Universal Router - const routerResult = uniswap.decode(data, toAddress); - if (routerResult) return routerResult; - - return null; -} +// decodeCalldata is now in ../../shared/decodeCalldata.js function showTxApproval(details) { const toAddr = details.txParams.to; -- 2.49.1 From 497d011b3c5a3e7033c360e1a415c0b6cb46e447 Mon Sep 17 00:00:00 2001 From: clawbot Date: Fri, 27 Feb 2026 12:05:17 -0800 Subject: [PATCH 3/4] Classify transactions by decoded calldata in history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the shared decodeCalldata module to detect Uniswap swaps, token approvals, and unknown contract calls in parseTx. Swaps now show 'Swap TOKEN_A → TOKEN_B' instead of 'Sent'. Unknown contract calls show 'Contract Call'. Raw input and decoded data are passed through for the detail view. --- src/shared/transactions.js | 43 ++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/shared/transactions.js b/src/shared/transactions.js index f926784..dee0d06 100644 --- a/src/shared/transactions.js +++ b/src/shared/transactions.js @@ -9,6 +9,7 @@ const { formatEther, formatUnits } = require("ethers"); const { log, debugFetch } = require("./log"); const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = require("./tokenList"); +const { decodeCalldata } = require("./decodeCalldata"); function formatTxValue(val) { const parts = val.split("."); @@ -23,6 +24,7 @@ function parseTx(tx, addrLower) { const rawWei = tx.value || "0"; const toIsContract = tx.to?.is_contract || false; const method = tx.method || null; + const rawInput = tx.raw_input || null; // For contract calls, produce a meaningful label instead of "0.0000 ETH" let symbol = "ETH"; @@ -32,14 +34,41 @@ function parseTx(tx, addrLower) { let rawUnit = "wei"; let direction = from.toLowerCase() === addrLower ? "sent" : "received"; let directionLabel = direction === "sent" ? "Sent" : "Received"; - if (toIsContract && method && method !== "transfer") { - const token = TOKEN_BY_ADDRESS.get(to.toLowerCase()); - if (token) { - symbol = token.symbol; + let decoded = null; + + if (rawInput && rawInput !== "0x" && rawInput.length >= 10) { + decoded = decodeCalldata(rawInput, to); + if (decoded) { + // Uniswap swaps: show "Swap" with token pair + if (decoded.name && decoded.name.startsWith("Swap")) { + direction = "contract"; + directionLabel = decoded.name; + value = ""; + exactValue = ""; + rawAmount = ""; + rawUnit = ""; + } else if (decoded.name === "Token Approval") { + direction = "contract"; + directionLabel = "Approve"; + value = ""; + exactValue = ""; + rawAmount = ""; + rawUnit = ""; + } + // Token Transfer: keep as Sent/Received (handled by token transfer overlay) + } else if (toIsContract && method && method !== "transfer") { + // Unknown contract call + direction = "contract"; + directionLabel = "Contract Call"; + value = ""; + exactValue = ""; + rawAmount = ""; + rawUnit = ""; } - const label = method.charAt(0).toUpperCase() + method.slice(1); + } else if (toIsContract && method && method !== "transfer") { + // Contract call without raw input data direction = "contract"; - directionLabel = label; + directionLabel = "Contract Call"; value = ""; exactValue = ""; rawAmount = ""; @@ -65,6 +94,8 @@ function parseTx(tx, addrLower) { holders: null, isContractCall: toIsContract, method: method, + rawInput: rawInput, + decoded: decoded, }; } -- 2.49.1 From efc5404d6dddc1941915543a3ff430d8e41b061b Mon Sep 17 00:00:00 2001 From: clawbot Date: Fri, 27 Feb 2026 12:05:21 -0800 Subject: [PATCH 4/4] Show decoded calldata in transaction detail view Add decoded transaction summary (matching the approval screen format) to the transaction detail view. For unknown contract calls, show 'Unknown Contract Call' label with full raw calldata hex (not truncated). --- src/popup/index.html | 17 +++++++++ src/popup/views/transactionDetail.js | 56 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/popup/index.html b/src/popup/index.html index 6922bcd..b8f6f4e 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -943,6 +943,23 @@
Transaction hash
+ + diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js index 8ecbfe3..9961e28 100644 --- a/src/popup/views/transactionDetail.js +++ b/src/popup/views/transactionDetail.js @@ -12,6 +12,8 @@ const { timeAgo, } = require("./helpers"); const { state } = require("../../shared/state"); +const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList"); +const { decodeCalldata } = require("../../shared/decodeCalldata"); const makeBlockie = require("ethereum-blockies-base64"); const EXT_ICON = @@ -85,6 +87,8 @@ function show(tx) { fromEns: tx.fromEns || null, toEns: tx.toEns || null, directionLabel: tx.directionLabel || null, + rawInput: tx.rawInput || null, + decoded: tx.decoded || null, }, }; render(); @@ -124,6 +128,58 @@ function render() { $("tx-detail-time").textContent = isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")"; $("tx-detail-status").textContent = tx.isError ? "Failed" : "Success"; + + // Decoded calldata section — matches approval screen format + const decodedEl = $("tx-detail-decoded"); + const rawDataEl = $("tx-detail-rawdata-section"); + const decoded = tx.decoded; + + if (decoded) { + $("tx-detail-action").textContent = decoded.name; + let detailsHtml = ""; + if (decoded.description) { + detailsHtml += `
${escapeHtml(decoded.description)}
`; + } + for (const d of decoded.details) { + detailsHtml += `
`; + detailsHtml += `
${escapeHtml(d.label)}
`; + if (d.address) { + if (d.isToken) { + const t = TOKEN_BY_ADDRESS.get(d.address.toLowerCase()); + const label = t ? t.symbol : "Unknown token"; + detailsHtml += `
${escapeHtml(label)}
`; + } + const dot = addressDotHtml(d.address); + const link = `https://etherscan.io/address/${d.address}`; + detailsHtml += + `
${dot}` + + `${escapeHtml(d.address)}` + + `${EXT_ICON}
`; + } else { + detailsHtml += `
${escapeHtml(d.value)}
`; + } + detailsHtml += `
`; + } + $("tx-detail-decoded-details").innerHTML = detailsHtml; + decodedEl.classList.remove("hidden"); + } else { + decodedEl.classList.add("hidden"); + } + + // Raw calldata section — shown for unknown contract calls (full, not truncated) + if (tx.rawInput && tx.rawInput !== "0x" && !decoded) { + $("tx-detail-rawdata").textContent = tx.rawInput; + rawDataEl.classList.remove("hidden"); + // Label as unknown contract call + $("tx-detail-action").textContent = "Unknown Contract Call"; + $("tx-detail-decoded-details").innerHTML = ""; + decodedEl.classList.remove("hidden"); + } else if (!decoded) { + rawDataEl.classList.add("hidden"); + } else { + rawDataEl.classList.add("hidden"); + } + showView("transaction"); document -- 2.49.1