// Decode a transaction's calldata into the summary that TxApproval and // TransactionDetail show: ERC-20 `approve` and `transfer` calls here, and // Uniswap Universal Router calls through uniswap.js. The summary is data; the // views turn its addresses into HTML. const { formatUnits, Interface } = require("ethers"); const { ERC20_ABI } = require("./constants"); const { tokenSourcesByChainId } = require("./state"); const { resolveTokenDecimals, resolveTokenSymbol, unknownDecimalsAmount, } = require("./approvalAmount"); // Four decimals, with the nonzero floor the approval screens hold. const { truncateAmountNeverZero } = require("./amountDisplay"); const uniswap = require("./uniswap"); const erc20Iface = new Interface(ERC20_ABI); // The amount line for a decoded ERC-20 call. With a known scale it is the // token quantity; with `decimals` null it is the base-unit integer with the // unknown scale stated, because formatting it with an assumed scale is what // showed a 5,000-token transfer as `0.0000`. `raw` is what the status screens // carry, `display` is what the approval screen shows. function tokenAmountText(rawAmount, decimals, symbol) { if (decimals === null) { const unknown = unknownDecimalsAmount(rawAmount); return { raw: unknown, display: unknown }; } const formatted = truncateAmountNeverZero(formatUnits(rawAmount, decimals)); return { raw: formatted, display: formatted + (symbol ? " " + symbol : ""), }; } // Try to decode calldata sent from `fromAddress` to `toAddress` on the network // with chain id `chainId`, using known ABIs. // Returns { name, description, details } or null. function decodeCalldata(data, toAddress, chainId, fromAddress) { if (!data || data === "0x" || data.length < 10) return null; // Where a token's name and scale are looked for, for every decoder below: // what the wallet knows of the tokens on the network with `chainId`, which // need not be the active one. The ERC-20 amount line and the swap's Amount // and Min. received lines resolve the scale the same way, and refuse to // format the same way when it is nowhere. const decimalsSources = tokenSourcesByChainId(chainId); // Try ERC-20 (approve / transfer) try { const parsed = erc20Iface.parseTransaction({ data }); if (parsed) { const tokenSymbol = resolveTokenSymbol(toAddress, decimalsSources); // null when no source knows this token's scale. It is not // defaulted to 18: an amount formatted with a guessed scale is // the wrong number, and for a token with fewer decimals than the // guess it is the wrong number in the direction that reads as // zero. See tokenAmountText(). const tokenDecimals = resolveTokenDecimals( toAddress, decimalsSources, ); 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; // An unbounded allowance needs no scale to describe, so it is // still named rather than refused. const amount = isUnlimited ? { raw: "Unlimited", display: "Unlimited" } : tokenAmountText(rawAmount, tokenDecimals, 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: amount.display, rawValue: amount.raw, }, ], }; } if (parsed.name === "transfer") { const to = parsed.args[0]; const rawAmount = parsed.args[1]; const amount = tokenAmountText( rawAmount, tokenDecimals, 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: amount.display, rawValue: amount.raw, }, ], }; } } } catch { // Not ERC-20 — fall through } // Try Uniswap Universal Router const routerResult = uniswap.decode( data, toAddress, chainId, decimalsSources, fromAddress, ); if (routerResult) return routerResult; return null; } module.exports = { decodeCalldata, tokenAmountText };