const { AbiCoder, Interface, solidityPacked } = require("ethers"); const uniswap = require("../src/shared/uniswap"); const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af"; const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; const USER_ADDR = "0x66133E8ea0f5D1d612D2502a968757D1048c214a"; // AutistMask's first-ever swap, 2026-02-27. // Swapped USDT for ETH via Uniswap V4 Universal Router. // https://etherscan.io/tx/0x6749f50c4e8f975b6d14780d5f539cf151d1594796ac49b7d6a5348ba0735e77 const FIRST_SWAP_CALLDATA = "0x3593564c" + "000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0" + "0000000000000000000000000000000000000000000000000000000069a1550f00000000000000000000000000000000000000000000000000000000000000020a10000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c0" + "0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000ffffffffffffffffffffffffffffffffffffffff" + "0000000000000000000000000000000000000000000000000000000069c8daf6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066a9893cc07d91d95644aedd05d03f95e1dba8af" + "0000000000000000000000000000000000000000000000000000000069a154fe00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000041" + "230249bb7133205db7b2389b587c723cc182302907b9545dc40c59c33ad1d53078a65732f4182fedbc0d9d85c51d580bdc93db3556fac38f18e140da47d0eb631c00000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003" + "070b0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220" + "00000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7" + "0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000007a1200000000000000000000000000000000000000000000000000000dcb050d338e7" + "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064" + "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0" + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "66133e8ea0f5d1d612d2502a968757d1048c214a0000000000000000000000000000000000000000000000000000000000000000756e6978000000000012"; const coder = AbiCoder.defaultAbiCoder(); const routerIface = new Interface([ "function execute(bytes commands, bytes[] inputs, uint256 deadline)", ]); // Helper: build a minimal execute() calldata from commands + inputs function buildExecute(commands, inputs, deadline) { return routerIface.encodeFunctionData("execute", [ commands, inputs, deadline, ]); } // Helper: encode a PERMIT2_PERMIT input (command 0x0a) function encodePermit2(token, amount, spender) { return coder.encode( [ "tuple(tuple(address,uint160,uint48,uint48),address,uint256)", "bytes", ], [[[token, amount, 0, 0], spender, 9999999999], "0x1234"], ); } // Helper: encode a BALANCE_CHECK_ERC20 input (command 0x0e) function encodeBalanceCheck(owner, token, minBalance) { return coder.encode( ["address", "address", "uint256"], [owner, token, minBalance], ); } // Helper: encode a WRAP_ETH input (command 0x0b) function encodeWrapEth(recipient, amount) { return coder.encode(["address", "uint256"], [recipient, amount]); } // Helper: encode a V2_SWAP_EXACT_IN input (command 0x08) function encodeV2SwapExactIn(recipient, amountIn, amountOutMin, pathAddrs) { return coder.encode( ["address", "uint256", "uint256", "address[]", "bool"], [recipient, amountIn, amountOutMin, pathAddrs, true], ); } // Helper: encode a V3_SWAP_EXACT_IN input (command 0x00) function encodeV3SwapExactIn(recipient, amountIn, amountOutMin, pathTokens) { // V3 path: token(20) + fee(3) + token(20) ... let pathHex = pathTokens[0].slice(2).toLowerCase(); for (let i = 1; i < pathTokens.length; i++) { pathHex += "000bb8"; // fee 3000 = 0x000bb8 pathHex += pathTokens[i].slice(2).toLowerCase(); } return coder.encode( ["address", "uint256", "uint256", "bytes", "bool"], [recipient, amountIn, amountOutMin, "0x" + pathHex, true], ); } // Helper: encode a V4_SWAP input (command 0x10) — just a passthrough blob function encodeV4Swap(actions, params) { return coder.encode(["bytes", "bytes[]"], [actions, params]); } describe("uniswap decoder", () => { test("returns null for non-execute calldata", () => { expect(uniswap.decode("0x", ROUTER_ADDR)).toBeNull(); expect(uniswap.decode("0xdeadbeef", ROUTER_ADDR)).toBeNull(); expect(uniswap.decode(null, ROUTER_ADDR)).toBeNull(); }); test("decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)", () => { const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR); expect(result).not.toBeNull(); expect(result.name).toBe("Swap USDT \u2192 ETH"); expect(result.description).toContain("Uniswap"); const labels = result.details.map((d) => d.label); expect(labels).toContain("Protocol"); expect(labels).toContain("Token In"); expect(labels).toContain("Steps"); expect(labels).toContain("Deadline"); const tokenIn = result.details.find((d) => d.label === "Token In"); expect(tokenIn.value).toContain("USDT"); expect(tokenIn.address.toLowerCase()).toBe(USDT_ADDR.toLowerCase()); const steps = result.details.find((d) => d.label === "Steps"); expect(steps.value).toContain("Permit2 Permit"); expect(steps.value).toContain("V4 Swap"); }); test("decodes V2_SWAP_EXACT_IN with known tokens", () => { const data = buildExecute( "0x08", // V2_SWAP_EXACT_IN [ encodeV2SwapExactIn( USER_ADDR, 1000000n, // 1 USDT (6 decimals) 500000000000000n, // 0.0005 ETH [USDT_ADDR, WETH_ADDR], ), ], 9999999999n, ); const result = uniswap.decode(data, ROUTER_ADDR); expect(result).not.toBeNull(); expect(result.name).toBe("Swap USDT \u2192 WETH"); const amount = result.details.find((d) => d.label === "Amount"); expect(amount.value).toBe("1.0000 USDT"); const minOut = result.details.find((d) => d.label === "Min. received"); expect(minOut.value).toContain("WETH"); }); test("decodes V3_SWAP_EXACT_IN with known tokens", () => { const data = buildExecute( "0x00", // V3_SWAP_EXACT_IN [ encodeV3SwapExactIn( USER_ADDR, 2000000n, // 2 USDT 1000000000000000n, // 0.001 ETH [USDT_ADDR, WETH_ADDR], ), ], 9999999999n, ); const result = uniswap.decode(data, ROUTER_ADDR); expect(result).not.toBeNull(); expect(result.name).toBe("Swap USDT \u2192 WETH"); }); test("decodes WRAP_ETH as ETH input", () => { const data = buildExecute( "0x0b", // WRAP_ETH [encodeWrapEth(ROUTER_ADDR, 1000000000000000000n)], 9999999999n, ); const result = uniswap.decode(data, ROUTER_ADDR); expect(result).not.toBeNull(); const tokenIn = result.details.find((d) => d.label === "Token In"); expect(tokenIn.value).toBe("ETH (native)"); const amount = result.details.find((d) => d.label === "Amount"); expect(amount.value).toContain("1.0000"); expect(amount.value).toContain("ETH"); }); test("decodes UNWRAP_WETH as ETH output", () => { const data = buildExecute( solidityPacked(["uint8", "uint8"], [0x08, 0x0c]), [ encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [ USDT_ADDR, WETH_ADDR, ]), encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH same encoding ], 9999999999n, ); const result = uniswap.decode(data, ROUTER_ADDR); expect(result).not.toBeNull(); // UNWRAP_WETH means output is native ETH expect(result.name).toBe("Swap USDT \u2192 ETH"); }); test("decodes BALANCE_CHECK_ERC20 for min output", () => { const data = buildExecute( solidityPacked(["uint8", "uint8"], [0x0b, 0x0e]), [ encodeWrapEth(ROUTER_ADDR, 1000000000000000000n), encodeBalanceCheck(USER_ADDR, USDT_ADDR, 2000000n), ], 9999999999n, ); const result = uniswap.decode(data, ROUTER_ADDR); expect(result).not.toBeNull(); const minOut = result.details.find((d) => d.label === "Min. received"); expect(minOut).toBeDefined(); expect(minOut.value).toContain("2.0000"); expect(minOut.value).toContain("USDT"); }); test("shows command names in steps", () => { const data = buildExecute( solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x10, 0x0c]), [ encodePermit2(USDT_ADDR, 1000000n, ROUTER_ADDR), encodeV4Swap("0x07", ["0x"]), encodeWrapEth(USER_ADDR, 0n), // reusing for UNWRAP_WETH ], 9999999999n, ); const result = uniswap.decode(data, ROUTER_ADDR); expect(result).not.toBeNull(); const steps = result.details.find((d) => d.label === "Steps"); expect(steps.value).toBe( "Permit2 Permit \u2192 V4 Swap \u2192 Unwrap WETH", ); }); test("formats permit amount when not unlimited", () => { const data = buildExecute( "0x0a", [encodePermit2(USDT_ADDR, 5000000n, ROUTER_ADDR)], 9999999999n, ); const result = uniswap.decode(data, ROUTER_ADDR); expect(result).not.toBeNull(); const amount = result.details.find((d) => d.label === "Amount"); expect(amount.value).toBe("5.0000 USDT"); }); test("handles unknown tokens gracefully", () => { const fakeToken = "0x1111111111111111111111111111111111111111"; const data = buildExecute( "0x0a", [encodePermit2(fakeToken, 1000000000000000000n, ROUTER_ADDR)], 9999999999n, ); const result = uniswap.decode(data, ROUTER_ADDR); expect(result).not.toBeNull(); expect(result.name).toBe("Uniswap Swap"); const tokenIn = result.details.find((d) => d.label === "Token In"); expect(tokenIn.value).toContain(fakeToken); }); });