From b2c947bfb7edf779b0e3fac622ffb477a6f8ccde Mon Sep 17 00:00:00 2001 From: clawbot Date: Fri, 27 Feb 2026 12:05:08 -0800 Subject: [PATCH] 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 };