All checks were successful
check / check (push) Successful in 4s
Replace stub error handlers with full approval flow for personal_sign, eth_sign, eth_signTypedData_v4, and eth_signTypedData. Uses toolbar popup only (no fallback window) and keeps sign approvals pending across popup close/reopen cycles so the user can respond via the toolbar icon.
334 lines
10 KiB
JavaScript
334 lines
10 KiB
JavaScript
// Decode Uniswap Universal Router execute() calldata into human-readable
|
|
// swap details. Designed to be extended with other DEX decoders later.
|
|
|
|
const { Interface, AbiCoder, getBytes, formatUnits } = require("ethers");
|
|
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
|
|
|
const coder = AbiCoder.defaultAbiCoder();
|
|
|
|
const ROUTER_IFACE = new Interface([
|
|
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
|
]);
|
|
|
|
// Universal Router command IDs (lower 5 bits of each command byte)
|
|
const COMMAND_NAMES = {
|
|
0x00: "V3 Swap (Exact In)",
|
|
0x01: "V3 Swap (Exact Out)",
|
|
0x02: "Permit2 Transfer",
|
|
0x03: "Permit2 Permit Batch",
|
|
0x04: "Sweep",
|
|
0x05: "Transfer",
|
|
0x06: "Pay Portion",
|
|
0x08: "V2 Swap (Exact In)",
|
|
0x09: "V2 Swap (Exact Out)",
|
|
0x0a: "Permit2 Permit",
|
|
0x0b: "Wrap ETH",
|
|
0x0c: "Unwrap WETH",
|
|
0x0d: "Permit2 Transfer Batch",
|
|
0x0e: "Balance Check",
|
|
0x10: "V4 Swap",
|
|
0x11: "V3 Position Mgr Permit",
|
|
0x12: "V3 Position Mgr Call",
|
|
0x13: "V4 Initialize Pool",
|
|
0x14: "V4 Position Mgr Call",
|
|
0x21: "Execute Sub-Plan",
|
|
};
|
|
|
|
function formatAmount(raw, decimals) {
|
|
const parts = formatUnits(raw, decimals).split(".");
|
|
if (parts.length === 1) return parts[0] + ".0000";
|
|
const dec = (parts[1] + "0000").slice(0, 4);
|
|
return parts[0] + "." + dec;
|
|
}
|
|
|
|
function tokenInfo(address) {
|
|
if (!address || address === "0x0000000000000000000000000000000000000000") {
|
|
return { symbol: "ETH", decimals: 18, address: null };
|
|
}
|
|
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
|
if (t) return { symbol: t.symbol, decimals: t.decimals, address };
|
|
return { symbol: null, decimals: 18, address };
|
|
}
|
|
|
|
// Decode PERMIT2_PERMIT (command 0x0a) input bytes.
|
|
// ABI: ((address token, uint160 amount, uint48 expiration, uint48 nonce),
|
|
// address spender, uint256 sigDeadline), bytes signature
|
|
function decodePermit2(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
[
|
|
"tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
|
|
"bytes",
|
|
],
|
|
input,
|
|
);
|
|
return { token: d[0][0][0], amount: d[0][0][1], spender: d[0][1] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode BALANCE_CHECK_ERC20 (command 0x0e) input bytes.
|
|
// ABI: (address owner, address token, uint256 minBalance)
|
|
function decodeBalanceCheck(input) {
|
|
try {
|
|
const d = coder.decode(["address", "address", "uint256"], input);
|
|
return { owner: d[0], token: d[1], minBalance: d[2] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode V2_SWAP_EXACT_IN (command 0x08) input bytes.
|
|
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
|
// address[] path, bool payerIsUser)
|
|
function decodeV2SwapExactIn(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
["address", "uint256", "uint256", "address[]", "bool"],
|
|
input,
|
|
);
|
|
return {
|
|
amountIn: d[1],
|
|
amountOutMin: d[2],
|
|
tokenIn: d[3][0],
|
|
tokenOut: d[3][d[3].length - 1],
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
|
|
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
|
// address[] path, bool payerIsUser)
|
|
function decodeV2SwapExactOut(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
["address", "uint256", "uint256", "address[]", "bool"],
|
|
input,
|
|
);
|
|
return {
|
|
amountOut: d[1],
|
|
amountInMax: d[2],
|
|
tokenIn: d[3][0],
|
|
tokenOut: d[3][d[3].length - 1],
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode V3 swap path (packed: token(20) + fee(3) + token(20) ...)
|
|
function decodeV3Path(pathHex) {
|
|
const hex = pathHex.startsWith("0x") ? pathHex.slice(2) : pathHex;
|
|
if (hex.length < 40) return null;
|
|
const tokenIn = "0x" + hex.slice(0, 40);
|
|
const tokenOut = "0x" + hex.slice(-40);
|
|
return { tokenIn, tokenOut };
|
|
}
|
|
|
|
// Decode V3_SWAP_EXACT_IN (command 0x00) input bytes.
|
|
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
|
// bytes path, bool payerIsUser)
|
|
function decodeV3SwapExactIn(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
["address", "uint256", "uint256", "bytes", "bool"],
|
|
input,
|
|
);
|
|
const path = decodeV3Path(d[3]);
|
|
if (!path) return null;
|
|
return {
|
|
amountIn: d[1],
|
|
amountOutMin: d[2],
|
|
tokenIn: path.tokenIn,
|
|
tokenOut: path.tokenOut,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode WRAP_ETH (command 0x0b) input bytes.
|
|
// ABI: (address recipient, uint256 amount)
|
|
function decodeWrapEth(input) {
|
|
try {
|
|
const d = coder.decode(["address", "uint256"], input);
|
|
return { amount: d[1] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Try to decode a Universal Router execute() call.
|
|
// Returns { name, description, details } matching the format used by
|
|
// the approval UI, or null if the calldata is not a recognised execute().
|
|
function decode(data, toAddress) {
|
|
try {
|
|
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
|
if (!parsed) return null;
|
|
|
|
const commandsBytes = getBytes(parsed.args[0]);
|
|
const inputs = parsed.args[1];
|
|
const deadline = parsed.args[2];
|
|
|
|
let inputToken = null;
|
|
let inputAmount = null;
|
|
let outputToken = null;
|
|
let minOutput = null;
|
|
let hasUnwrapWeth = false;
|
|
const commandNames = [];
|
|
|
|
for (let i = 0; i < commandsBytes.length; i++) {
|
|
const cmdId = commandsBytes[i] & 0x1f;
|
|
commandNames.push(
|
|
COMMAND_NAMES[cmdId] ||
|
|
"Command 0x" + cmdId.toString(16).padStart(2, "0"),
|
|
);
|
|
|
|
try {
|
|
if (cmdId === 0x0a) {
|
|
const p = decodePermit2(inputs[i]);
|
|
if (p) {
|
|
inputToken = p.token;
|
|
inputAmount = p.amount;
|
|
}
|
|
}
|
|
|
|
if (cmdId === 0x0e) {
|
|
const b = decodeBalanceCheck(inputs[i]);
|
|
if (b) {
|
|
outputToken = b.token;
|
|
minOutput = b.minBalance;
|
|
}
|
|
}
|
|
|
|
if (cmdId === 0x00) {
|
|
const s = decodeV3SwapExactIn(inputs[i]);
|
|
if (s) {
|
|
if (!inputToken) inputToken = s.tokenIn;
|
|
if (!outputToken) outputToken = s.tokenOut;
|
|
if (!inputAmount) inputAmount = s.amountIn;
|
|
if (!minOutput) minOutput = s.amountOutMin;
|
|
}
|
|
}
|
|
|
|
if (cmdId === 0x08) {
|
|
const s = decodeV2SwapExactIn(inputs[i]);
|
|
if (s) {
|
|
if (!inputToken) inputToken = s.tokenIn;
|
|
if (!outputToken) outputToken = s.tokenOut;
|
|
if (!inputAmount) inputAmount = s.amountIn;
|
|
if (!minOutput) minOutput = s.amountOutMin;
|
|
}
|
|
}
|
|
|
|
if (cmdId === 0x0b) {
|
|
const w = decodeWrapEth(inputs[i]);
|
|
if (w && !inputToken) {
|
|
inputToken =
|
|
"0x0000000000000000000000000000000000000000";
|
|
inputAmount = w.amount;
|
|
}
|
|
}
|
|
|
|
if (cmdId === 0x0c) {
|
|
hasUnwrapWeth = true;
|
|
}
|
|
} catch {
|
|
// Skip commands we can't decode
|
|
}
|
|
}
|
|
|
|
// Resolve token info
|
|
const inInfo = tokenInfo(inputToken);
|
|
const outInfo = hasUnwrapWeth
|
|
? { symbol: "ETH", decimals: 18, address: null }
|
|
: tokenInfo(outputToken);
|
|
|
|
const inSymbol = inInfo.symbol;
|
|
const outSymbol = outInfo.symbol;
|
|
|
|
const name =
|
|
inSymbol && outSymbol
|
|
? "Swap " + inSymbol + " \u2192 " + outSymbol
|
|
: "Uniswap Swap";
|
|
|
|
const details = [];
|
|
|
|
details.push({
|
|
label: "Protocol",
|
|
value: "Uniswap Universal Router",
|
|
address: toAddress,
|
|
});
|
|
|
|
if (inputToken && inInfo.address) {
|
|
const label = inSymbol
|
|
? inSymbol + " (" + inputToken + ")"
|
|
: inputToken;
|
|
details.push({
|
|
label: "Token In",
|
|
value: label,
|
|
address: inputToken,
|
|
isToken: true,
|
|
});
|
|
} else if (inSymbol === "ETH") {
|
|
details.push({ label: "Token In", value: "ETH (native)" });
|
|
}
|
|
|
|
if (inputAmount !== null && inputAmount !== undefined) {
|
|
const maxUint160 = BigInt(
|
|
"0xffffffffffffffffffffffffffffffffffffffff",
|
|
);
|
|
const amountStr =
|
|
inputAmount >= maxUint160
|
|
? "Unlimited"
|
|
: formatAmount(inputAmount, inInfo.decimals) +
|
|
(inSymbol ? " " + inSymbol : "");
|
|
details.push({ label: "Amount", value: amountStr });
|
|
}
|
|
|
|
if (outSymbol) {
|
|
if (outInfo.address) {
|
|
const label = outSymbol
|
|
? outSymbol + " (" + outputToken + ")"
|
|
: outputToken;
|
|
details.push({
|
|
label: "Token Out",
|
|
value: label,
|
|
address: outputToken,
|
|
isToken: true,
|
|
});
|
|
} else {
|
|
details.push({ label: "Token Out", value: outSymbol });
|
|
}
|
|
}
|
|
|
|
if (minOutput !== null && minOutput !== undefined) {
|
|
const minStr =
|
|
formatAmount(minOutput, outInfo.decimals) +
|
|
(outSymbol ? " " + outSymbol : "");
|
|
details.push({ label: "Min. received", value: minStr });
|
|
}
|
|
|
|
details.push({ label: "Steps", value: commandNames.join(" \u2192 ") });
|
|
|
|
const deadlineDate = new Date(Number(deadline) * 1000);
|
|
details.push({
|
|
label: "Deadline",
|
|
value: deadlineDate.toISOString().replace("T", " ").slice(0, 19),
|
|
});
|
|
|
|
return {
|
|
name,
|
|
description: "Swap via Uniswap Universal Router",
|
|
details,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = { decode };
|