diff --git a/src/shared/uniswap.js b/src/shared/uniswap.js index 1f7f041..76b6372 100644 --- a/src/shared/uniswap.js +++ b/src/shared/uniswap.js @@ -161,6 +161,157 @@ function decodeWrapEth(input) { } } +// V4 inner action IDs +const V4_SWAP_EXACT_IN_SINGLE = 0x06; +const V4_SWAP_EXACT_IN = 0x07; +const V4_SWAP_EXACT_OUT_SINGLE = 0x08; +const V4_SWAP_EXACT_OUT = 0x09; +const V4_SETTLE = 0x0b; +const V4_TAKE = 0x0e; + +// Decode V4_SWAP (command 0x10) input bytes. +// The input is ABI-encoded as (bytes actions, bytes[] params). +// We extract token addresses from SETTLE (input) and TAKE (output) sub-actions, +// and swap amounts from the swap sub-actions. +function decodeV4Swap(input) { + try { + const d = coder.decode(["bytes", "bytes[]"], input); + const actions = getBytes(d[0]); + const params = d[1]; + + let settleToken = null; + let takeToken = null; + let amountIn = null; + let amountOutMin = null; + + for (let i = 0; i < actions.length; i++) { + const actionId = actions[i]; + try { + if (actionId === V4_SETTLE) { + // SETTLE: (address currency, uint256 maxAmount, bool payerIsUser) + const s = coder.decode( + ["address", "uint256", "bool"], + params[i], + ); + settleToken = s[0]; + } else if (actionId === V4_TAKE) { + // TAKE: (address currency, address recipient, uint256 amount) + const t = coder.decode( + ["address", "address", "uint256"], + params[i], + ); + takeToken = t[0]; + } else if ( + actionId === V4_SWAP_EXACT_IN || + actionId === V4_SWAP_EXACT_IN_SINGLE + ) { + // Extract amounts from exact-in swap actions + if (actionId === V4_SWAP_EXACT_IN) { + // ExactInputParams: (address currencyIn, + // tuple(address,uint24,int24,address,bytes)[] path, + // uint128 amountIn, uint128 amountOutMin) + try { + const s = coder.decode( + [ + "tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)", + ], + params[i], + ); + if (!settleToken) settleToken = s[0][0]; + const path = s[0][1]; + if (path.length > 0 && !takeToken) { + takeToken = path[path.length - 1][0]; + } + if (!amountIn) amountIn = s[0][2]; + if (!amountOutMin) amountOutMin = s[0][3]; + } catch { + // Fall through — SETTLE/TAKE will provide tokens + } + } else { + // ExactInputSingleParams: (tuple(address,address,uint24,int24,address) poolKey, + // bool zeroForOne, uint128 amountIn, uint128 amountOutMin, bytes hookData) + try { + const s = coder.decode( + [ + "tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)", + ], + params[i], + ); + const poolKey = s[0][0]; + const zeroForOne = s[0][1]; + if (!settleToken) + settleToken = zeroForOne + ? poolKey[0] + : poolKey[1]; + if (!takeToken) + takeToken = zeroForOne + ? poolKey[1] + : poolKey[0]; + if (!amountIn) amountIn = s[0][2]; + if (!amountOutMin) amountOutMin = s[0][3]; + } catch { + // Fall through + } + } + } else if ( + actionId === V4_SWAP_EXACT_OUT || + actionId === V4_SWAP_EXACT_OUT_SINGLE + ) { + if (actionId === V4_SWAP_EXACT_OUT) { + try { + const s = coder.decode( + [ + "tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)", + ], + params[i], + ); + if (!takeToken) takeToken = s[0][0]; + const path = s[0][1]; + if (path.length > 0 && !settleToken) { + settleToken = path[path.length - 1][0]; + } + } catch { + // Fall through + } + } else { + try { + const s = coder.decode( + [ + "tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)", + ], + params[i], + ); + const poolKey = s[0][0]; + const zeroForOne = s[0][1]; + if (!settleToken) + settleToken = zeroForOne + ? poolKey[0] + : poolKey[1]; + if (!takeToken) + takeToken = zeroForOne + ? poolKey[1] + : poolKey[0]; + } catch { + // Fall through + } + } + } + } catch { + // Skip sub-actions we can't decode + } + } + + return { + tokenIn: settleToken, + tokenOut: takeToken, + amountIn, + amountOutMin, + }; + } 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(). @@ -233,6 +384,19 @@ function decode(data, toAddress) { } } + if (cmdId === 0x10) { + const v4 = decodeV4Swap(inputs[i]); + if (v4) { + if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn; + if (!outputToken && v4.tokenOut) + outputToken = v4.tokenOut; + if (!inputAmount && v4.amountIn) + inputAmount = v4.amountIn; + if (!minOutput && v4.amountOutMin) + minOutput = v4.amountOutMin; + } + } + if (cmdId === 0x0c) { hasUnwrapWeth = true; }