decodeCalldata() and tokenAmountText() move out of the TxApproval view into src/shared/calldataDecoder.js. TxApproval, TransactionDetail and the decoder tests import them from there. tokenSourcesByChainId() moves from the popup view helpers to src/shared/state.js, beside currentNetwork(), so the decoder requires nothing under src/popup/. It reads only the state singleton and the network table. Model: opus-5-5
156 lines
6.0 KiB
JavaScript
156 lines
6.0 KiB
JavaScript
// 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 };
|