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 };