diff --git a/src/components/transactions/batchUsdcSwap.js b/src/components/transactions/batchUsdcSwap.js
new file mode 100644
index 00000000..44c6802c
--- /dev/null
+++ b/src/components/transactions/batchUsdcSwap.js
@@ -0,0 +1,423 @@
+import { ethers } from 'ethers';
+import globalContext from '../..';
+import {
+ USDC_ADDRESS,
+ WETH_ADDRESS,
+ PERMIT2_ADDRESS,
+ UNIVERSAL_ROUTER,
+ ROUTER_AS_RECIPIENT,
+ DEFAULT_FEE_RECIPIENT,
+ DEFAULT_FEE_PERCENTAGE,
+ EMPTY_BYTES,
+ EIP5792_VERSION,
+ Actions,
+ isValidAddress,
+ parseUsdcAmount,
+ parseFeePercentage,
+ addAction,
+ encodeApprove,
+ encodePermit2Approve,
+} from './swapUtils';
+
+// Batch-specific constants
+const DEFAULT_USDC_AMOUNT = '0.5'; // 0.5 USDC
+
+// Helper function to get configuration values from UI
+function getConfigValues() {
+ const feeRecipientElement = document.getElementById(
+ 'batchUsdcSwapFeeRecipient',
+ );
+ const usdcAmountElement = document.getElementById('batchUsdcSwapAmount');
+ const feePercentageElement = document.getElementById(
+ 'batchUsdcSwapFeePercentage',
+ );
+
+ const feeRecipientInput = feeRecipientElement
+ ? feeRecipientElement.value.trim()
+ : '';
+ const usdcAmountInput = usdcAmountElement
+ ? usdcAmountElement.value.trim()
+ : '';
+ const feePercentageInput = feePercentageElement
+ ? feePercentageElement.value.trim()
+ : '';
+
+ // Use defaults if inputs are empty or invalid
+ const feeRecipient =
+ feeRecipientInput && isValidAddress(feeRecipientInput)
+ ? feeRecipientInput
+ : DEFAULT_FEE_RECIPIENT;
+
+ const usdcAmount =
+ parseUsdcAmount(usdcAmountInput) ||
+ ethers.utils.parseUnits(DEFAULT_USDC_AMOUNT, 6);
+
+ const feeBips =
+ parseFeePercentage(feePercentageInput) || DEFAULT_FEE_PERCENTAGE * 100;
+
+ return {
+ feeRecipient,
+ usdcAmount,
+ feeBips,
+ usdcAmountDisplay: usdcAmountInput || DEFAULT_USDC_AMOUNT,
+ feePercentageDisplay:
+ feePercentageInput || DEFAULT_FEE_PERCENTAGE.toString(),
+ };
+}
+
+// Build swap calldata for USDC → ETH (multihop)
+function buildSwapCalldata(usdcAmount, feeRecipient, feeBips) {
+ const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes from now
+
+ let v4Actions = EMPTY_BYTES;
+ const v4Params = [];
+
+ const amountOutMinimum = '0x0'; // No slippage protection
+
+ const path = [
+ {
+ intermediateCurrency: WETH_ADDRESS,
+ fee: '3000',
+ tickSpacing: 60,
+ hooks: '0x0000000000000000000000000000000000000000',
+ hookData: '0x',
+ },
+ ];
+
+ // Action 1: Swap USDC for ETH
+ const swapExactIn = addAction(Actions.SWAP_EXACT_IN, [
+ {
+ path,
+ amountIn: usdcAmount,
+ amountOutMinimum,
+ currencyIn: USDC_ADDRESS,
+ },
+ ]);
+ v4Actions = v4Actions.concat(swapExactIn.newAction);
+ v4Params.push(swapExactIn.newParam);
+
+ // Action 2: Settle USDC payment
+ const settleAll = addAction(Actions.SETTLE_ALL, [
+ USDC_ADDRESS, // USDC
+ usdcAmount,
+ ]);
+ v4Actions = v4Actions.concat(settleAll.newAction);
+ v4Params.push(settleAll.newParam);
+
+ // Action 3: Take fee portion of ETH output
+ const takePortion = addAction(Actions.TAKE_PORTION, [
+ WETH_ADDRESS, // ETH
+ feeRecipient,
+ feeBips,
+ ]);
+ v4Actions = v4Actions.concat(takePortion.newAction);
+ v4Params.push(takePortion.newParam);
+
+ const take = addAction(Actions.TAKE, [
+ WETH_ADDRESS, // ETH
+ ROUTER_AS_RECIPIENT,
+ amountOutMinimum,
+ ]);
+ v4Actions = v4Actions.concat(take.newAction);
+ v4Params.push(take.newParam);
+
+ // Encode the V4_SWAP input
+ const v4SwapInput = ethers.utils.defaultAbiCoder.encode(
+ ['bytes', 'bytes[]'],
+ [v4Actions, v4Params],
+ );
+
+ const unwrapWethInput = ethers.utils.defaultAbiCoder.encode(
+ ['address', 'uint256'],
+ [globalContext.accounts[0], amountOutMinimum],
+ );
+
+ const commands = '0x100c'; // V4_SWAP + UNWRAP_WETH
+ const inputs = [v4SwapInput, unwrapWethInput];
+
+ // Encode the execute function call
+ const iface = new ethers.utils.Interface([
+ 'function execute(bytes commands, bytes[] inputs, uint256 deadline)',
+ ]);
+
+ return iface.encodeFunctionData('execute', [commands, inputs, deadline]);
+}
+
+// Build all three calls for the batch
+function buildBatchCalls(usdcAmount, feeRecipient, feeBips) {
+ const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes from now
+
+ // Call 1: Approve Permit2 to spend USDC
+ const call1 = {
+ to: USDC_ADDRESS,
+ data: encodeApprove(PERMIT2_ADDRESS, usdcAmount),
+ value: '0x0',
+ };
+
+ // Call 2: Approve Universal Router on Permit2
+ const call2 = {
+ to: PERMIT2_ADDRESS,
+ data: encodePermit2Approve(
+ USDC_ADDRESS,
+ UNIVERSAL_ROUTER,
+ usdcAmount,
+ deadline,
+ ),
+ value: '0x0',
+ };
+
+ // Call 3: Execute swap
+ const call3 = {
+ to: UNIVERSAL_ROUTER,
+ data: buildSwapCalldata(usdcAmount, feeRecipient, feeBips),
+ value: '0x0',
+ };
+
+ return [call1, call2, call3];
+}
+
+export function batchUsdcSwapComponent(parentContainer) {
+ parentContainer.insertAdjacentHTML(
+ 'beforeend',
+ `
+
+
+
+ Batch USDC → ETH Swap (Mainnet only)
+
+
+
+ ⚠️ Atomic batch via EIP-5792 wallet_sendCalls
+
+
+
+ Current Swap Details:
+
+ • From: ${DEFAULT_USDC_AMOUNT} USDC
+ • To: ETH
+ • Fee: ${DEFAULT_FEE_PERCENTAGE}% of output
+ • Approvals: Exact amount only
+ • Slippage: No protection (amountOutMin = 0)
+
+
+
+
+
+
+
+ Configuration (Optional)
+
+
+
+
+
+ Leave empty for default (${DEFAULT_USDC_AMOUNT} USDC)
+
+
+
+
+
+ Leave empty for default (${DEFAULT_FEE_PERCENTAGE}%)
+
+
+
+
+
+ Leave empty for default address
+
+
+
+
+
+
+
+
+
+ Status: Not executed
+
+
+
+ Batch ID: -
+
+
+
+
+
+
+
+
+
+
`,
+ );
+
+ const executeButton = document.getElementById('batchUsdcSwapExecuteButton');
+ const statusDisplay = document.getElementById('batchUsdcSwapStatus');
+ const batchIdDisplay = document.getElementById('batchUsdcSwapBatchId');
+ const checkStatusButton = document.getElementById(
+ 'batchUsdcSwapCheckStatusButton',
+ );
+ const resetButton = document.getElementById('batchUsdcSwapResetButton');
+ const usdcAmountInput = document.getElementById('batchUsdcSwapAmount');
+ const feePercentageInput = document.getElementById(
+ 'batchUsdcSwapFeePercentage',
+ );
+ const feeRecipientInput = document.getElementById(
+ 'batchUsdcSwapFeeRecipient',
+ );
+ const displayUsdcAmount = document.getElementById('displayUsdcAmount');
+ const displayFeePercentage = document.getElementById(
+ 'displayBatchFeePercentage',
+ );
+ const errorContainer = document.getElementById('batchUsdcSwapErrorContainer');
+ const errorOutput = document.getElementById('batchUsdcSwapError');
+
+ let currentBatchId = null;
+
+ // Function to update the swap details display
+ function updateSwapDetailsDisplay() {
+ const config = getConfigValues();
+ displayUsdcAmount.textContent = config.usdcAmountDisplay;
+ displayFeePercentage.textContent = config.feePercentageDisplay;
+ }
+
+ // Add event listeners for input changes to update display
+ usdcAmountInput.addEventListener('input', updateSwapDetailsDisplay);
+ feePercentageInput.addEventListener('input', updateSwapDetailsDisplay);
+
+ // Reset button handler
+ resetButton.addEventListener('click', function () {
+ usdcAmountInput.value = '';
+ feePercentageInput.value = '';
+ feeRecipientInput.value = '';
+ updateSwapDetailsDisplay();
+ });
+
+ // Enable/disable based on network
+ document.addEventListener('newChainIdInt', function (e) {
+ executeButton.disabled = e.detail.chainIdInt !== 1;
+ });
+
+ document.addEventListener('globalConnectionChange', function (e) {
+ if (e.detail.connected && globalContext.chainIdInt === 1) {
+ executeButton.disabled = false;
+ }
+ });
+
+ document.addEventListener('disableAndClear', function () {
+ executeButton.disabled = true;
+ });
+
+ // Check status button handler
+ checkStatusButton.onclick = () => {
+ if (currentBatchId) {
+ const requestIdInput = document.getElementById('eip5792RequestIdInput');
+ if (requestIdInput) {
+ requestIdInput.value = currentBatchId;
+ // Optionally scroll to the status section
+ requestIdInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ }
+ };
+
+ // Execute batch swap
+ executeButton.onclick = async () => {
+ try {
+ statusDisplay.innerHTML = 'Building batch transaction...';
+ errorContainer.hidden = true;
+ errorOutput.innerHTML = '';
+
+ // Get configuration values
+ const config = getConfigValues();
+
+ console.log('=== Batch USDC → ETH Swap ===');
+ console.log('USDC Amount:', config.usdcAmount.toString());
+ console.log('Fee Recipient:', config.feeRecipient);
+ console.log('Fee %:', config.feeBips / 100, '%');
+
+ // Build the batch calls
+ const calls = buildBatchCalls(
+ config.usdcAmount,
+ config.feeRecipient,
+ config.feeBips,
+ );
+
+ console.log('Batch calls:', calls);
+
+ statusDisplay.innerHTML = 'Sending batch transaction...';
+
+ // Send the batch using wallet_sendCalls
+ const result = await globalContext.provider.request({
+ method: 'wallet_sendCalls',
+ params: [
+ {
+ version: EIP5792_VERSION,
+ from: globalContext.accounts[0],
+ chainId: `0x${globalContext.chainIdInt.toString(16)}`,
+ atomicRequired: true,
+ calls,
+ },
+ ],
+ });
+
+ console.log('Batch result:', result);
+
+ currentBatchId = result.id;
+ statusDisplay.innerHTML = 'Batch submitted successfully!';
+ batchIdDisplay.innerHTML = currentBatchId;
+ checkStatusButton.disabled = false;
+ } catch (error) {
+ console.error('Batch swap error:', error);
+ statusDisplay.innerHTML = 'Error';
+ errorContainer.hidden = false;
+ errorOutput.innerHTML = `Error: ${error.message || 'Transaction failed'}`;
+ batchIdDisplay.innerHTML = '-';
+ checkStatusButton.disabled = true;
+ }
+ };
+}
diff --git a/src/components/transactions/index.js b/src/components/transactions/index.js
index 07d74fd9..ee8456c9 100644
--- a/src/components/transactions/index.js
+++ b/src/components/transactions/index.js
@@ -4,3 +4,4 @@ export * from './erc721';
export * from './erc1155';
export * from './send';
export * from './swapComparison';
+export * from './batchUsdcSwap';
diff --git a/src/components/transactions/swapComparison.js b/src/components/transactions/swapComparison.js
index 74b5000e..30dc201c 100644
--- a/src/components/transactions/swapComparison.js
+++ b/src/components/transactions/swapComparison.js
@@ -1,99 +1,23 @@
import { ethers } from 'ethers';
import globalContext from '../..';
-
-// Constants
-const UNIVERSAL_ROUTER = '0x66a9893cc07d91d95644aedd05d03f95e1dba8af';
-const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
-const DEFAULT_FEE_RECIPIENT = '0x58c51ee8998e8ef06362df26a0d966bbd0cf5113';
+import {
+ UNIVERSAL_ROUTER,
+ USDC_ADDRESS,
+ DEFAULT_FEE_RECIPIENT,
+ DEFAULT_FEE_PERCENTAGE,
+ EMPTY_BYTES,
+ Actions,
+ isValidAddress,
+ parseEthAmount,
+ parseFeePercentage,
+ stripLeadingZeros,
+ addAction,
+} from './swapUtils';
+
+// Comparison-specific constants
const DEFAULT_ETH_AMOUNT = '0.0003'; // 0.0003 ETH
-const DEFAULT_FEE_PERCENTAGE = 50; // 50%
-const EMPTY_BYTES = '0x';
-
-const Actions = {
- SWAP_EXACT_IN_SINGLE: 0x06,
- SETTLE_ALL: 0x0c,
- TAKE_PORTION: 0x10,
- TAKE_ALL: 0x0f,
-};
-
-const POOL_KEY_STRUCT =
- '(address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks)';
-const SWAP_EXACT_IN_SINGLE_STRUCT = `(${POOL_KEY_STRUCT} poolKey,bool zeroForOne,uint128 amountIn,uint128 amountOutMinimum,bytes hookData)`;
-
-const V4_BASE_ACTIONS_ABI_DEFINITION = {
- [Actions.SWAP_EXACT_IN_SINGLE]: [
- {
- name: 'swap',
- type: SWAP_EXACT_IN_SINGLE_STRUCT,
- },
- ],
- [Actions.SETTLE_ALL]: [
- { name: 'currency', type: 'address' },
- { name: 'maxAmount', type: 'uint256' },
- ],
- [Actions.TAKE_PORTION]: [
- { name: 'currency', type: 'address' },
- { name: 'recipient', type: 'address' },
- { name: 'bips', type: 'uint256' },
- ],
- [Actions.TAKE_ALL]: [
- { name: 'currency', type: 'address' },
- { name: 'minAmount', type: 'uint256' },
- ],
-};
-
-// Helper functions for input validation and parsing
-function stripLeadingZeros(hexString) {
- // Handle 0x0 or 0x00...0 -> return 0x0
- if (hexString === '0x' || /^0x0+$/u.test(hexString)) {
- return '0x0';
- }
- // Remove leading zeros: 0x0110d... -> 0x110d...
- return hexString.replace(/^0x0+/u, '0x');
-}
-
-function isValidAddress(address) {
- return ethers.utils.isAddress(address);
-}
-
-function parseEthAmount(input) {
- const value = input.trim();
- if (!value) {
- return null;
- }
-
- try {
- const parsed = parseFloat(value);
- if (isNaN(parsed) || parsed < 0) {
- return null;
- }
- // Convert ETH to wei and then to hex, stripping leading zeros
- return stripLeadingZeros(
- ethers.utils.parseEther(value.toString()).toHexString(),
- );
- } catch {
- return null;
- }
-}
-
-function parseFeePercentage(input) {
- const value = input.trim();
- if (!value) {
- return null;
- }
-
- try {
- const parsed = parseFloat(value);
- if (isNaN(parsed) || parsed < 0 || parsed > 100) {
- return null;
- }
- // Convert percentage to basis points (1% = 100 basis points)
- return Math.round(parsed * 100);
- } catch {
- return null;
- }
-}
+// Helper function to get configuration values from UI
function getConfigValues() {
const feeRecipientElement = document.getElementById('feeRecipientInput');
const ethAmountElement = document.getElementById('ethAmountInput');
@@ -132,25 +56,6 @@ function getConfigValues() {
};
}
-function createAction(action, parameters) {
- const encodedInput = ethers.utils.defaultAbiCoder.encode(
- V4_BASE_ACTIONS_ABI_DEFINITION[action].map((v) => v.type),
- parameters,
- );
- return { action, encodedInput };
-}
-
-function addAction(type, parameters) {
- const command = createAction(type, parameters);
- const newParam = command.encodedInput;
- const newAction = command.action.toString(16).padStart(2, '0');
-
- return {
- newParam,
- newAction,
- };
-}
-
export function swapComparisonComponent(parentContainer) {
parentContainer.insertAdjacentHTML(
'beforeend',
diff --git a/src/components/transactions/swapUtils.js b/src/components/transactions/swapUtils.js
new file mode 100644
index 00000000..e165ab0f
--- /dev/null
+++ b/src/components/transactions/swapUtils.js
@@ -0,0 +1,258 @@
+import { ethers } from 'ethers';
+
+// ============================================================
+// Constants
+// ============================================================
+
+// Token Addresses
+export const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
+export const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
+
+// Contract Addresses
+export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
+export const UNIVERSAL_ROUTER = '0x66a9893cc07d91d95644aedd05d03f95e1dba8af';
+export const ROUTER_AS_RECIPIENT = '0x0000000000000000000000000000000000000002';
+
+// Defaults
+export const DEFAULT_FEE_RECIPIENT =
+ '0x58c51ee8998e8ef06362df26a0d966bbd0cf5113';
+export const DEFAULT_FEE_PERCENTAGE = 50; // 50%
+export const EMPTY_BYTES = '0x';
+export const EIP5792_VERSION = '2.0.0';
+
+// Action Types
+export const Actions = {
+ SWAP_EXACT_IN_SINGLE: 0x06,
+ SWAP_EXACT_IN: 0x07,
+ SETTLE_ALL: 0x0c,
+ TAKE: 0x0e,
+ TAKE_ALL: 0x0f,
+ TAKE_PORTION: 0x10,
+};
+
+// ============================================================
+// ABI Type Definitions
+// ============================================================
+
+export const POOL_KEY_STRUCT =
+ '(address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks)';
+
+export const PATH_KEY_STRUCT =
+ '(address intermediateCurrency,uint256 fee,int24 tickSpacing,address hooks,bytes hookData)';
+
+export const SWAP_EXACT_IN_SINGLE_STRUCT = `(${POOL_KEY_STRUCT} poolKey,bool zeroForOne,uint128 amountIn,uint128 amountOutMinimum,bytes hookData)`;
+
+export const SWAP_EXACT_IN_STRUCT = `(address currencyIn,${PATH_KEY_STRUCT}[] path,uint128 amountIn,uint128 amountOutMinimum)`;
+
+export const V4_BASE_ACTIONS_ABI_DEFINITION = {
+ [Actions.SWAP_EXACT_IN_SINGLE]: [
+ {
+ name: 'swap',
+ type: SWAP_EXACT_IN_SINGLE_STRUCT,
+ },
+ ],
+ [Actions.SWAP_EXACT_IN]: [
+ {
+ name: 'swap',
+ type: SWAP_EXACT_IN_STRUCT,
+ },
+ ],
+ [Actions.SETTLE_ALL]: [
+ { name: 'currency', type: 'address' },
+ { name: 'maxAmount', type: 'uint256' },
+ ],
+ [Actions.TAKE]: [
+ { name: 'currency', type: 'address' },
+ { name: 'recipient', type: 'address' },
+ { name: 'amount', type: 'uint256' },
+ ],
+ [Actions.TAKE_ALL]: [
+ { name: 'currency', type: 'address' },
+ { name: 'minAmount', type: 'uint256' },
+ ],
+ [Actions.TAKE_PORTION]: [
+ { name: 'currency', type: 'address' },
+ { name: 'recipient', type: 'address' },
+ { name: 'bips', type: 'uint256' },
+ ],
+};
+
+// ============================================================
+// Validation Helpers
+// ============================================================
+
+/**
+ * Validates if a string is a valid Ethereum address
+ * @param {string} address - Address to validate
+ * @returns {boolean} True if valid address
+ */
+export function isValidAddress(address) {
+ return ethers.utils.isAddress(address);
+}
+
+/**
+ * Removes leading zeros from hex string
+ * @param {string} hexString - Hex string to strip
+ * @returns {string} Hex string without leading zeros
+ */
+export function stripLeadingZeros(hexString) {
+ // Handle 0x0 or 0x00...0 -> return 0x0
+ if (hexString === '0x' || /^0x0+$/u.test(hexString)) {
+ return '0x0';
+ }
+ // Remove leading zeros: 0x0110d... -> 0x110d...
+ return hexString.replace(/^0x0+/u, '0x');
+}
+
+// ============================================================
+// Parsing Helpers
+// ============================================================
+
+/**
+ * Parses USDC amount from user input
+ * @param {string} input - User input string
+ * @returns {ethers.BigNumber|null} Parsed amount in USDC units (6 decimals) or null if invalid
+ */
+export function parseUsdcAmount(input) {
+ const value = input.trim();
+ if (!value) {
+ return null;
+ }
+
+ try {
+ const parsed = parseFloat(value);
+ if (isNaN(parsed) || parsed <= 0) {
+ return null;
+ }
+ // USDC has 6 decimals
+ const amountInWei = ethers.utils.parseUnits(value.toString(), 6);
+ return amountInWei;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Parses ETH amount from user input and returns as hex string
+ * @param {string} input - User input string
+ * @returns {string|null} Parsed amount as hex string or null if invalid
+ */
+export function parseEthAmount(input) {
+ const value = input.trim();
+ if (!value) {
+ return null;
+ }
+
+ try {
+ const parsed = parseFloat(value);
+ if (isNaN(parsed) || parsed < 0) {
+ return null;
+ }
+ // Convert ETH to wei and then to hex, stripping leading zeros
+ return stripLeadingZeros(
+ ethers.utils.parseEther(value.toString()).toHexString(),
+ );
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Parses fee percentage from user input and converts to basis points
+ * @param {string} input - User input string (0-100)
+ * @returns {number|null} Fee in basis points (1% = 100 bips) or null if invalid
+ */
+export function parseFeePercentage(input) {
+ const value = input.trim();
+ if (!value) {
+ return null;
+ }
+
+ try {
+ const parsed = parseFloat(value);
+ if (isNaN(parsed) || parsed < 0 || parsed > 100) {
+ return null;
+ }
+ // Convert percentage to basis points (1% = 100 basis points)
+ return Math.round(parsed * 100);
+ } catch {
+ return null;
+ }
+}
+
+// ============================================================
+// Action Builders
+// ============================================================
+
+/**
+ * Creates an action with encoded parameters
+ * @param {number} action - Action type from Actions enum
+ * @param {Array} parameters - Array of parameters to encode
+ * @returns {{action: number, encodedInput: string}} Action object with encoded input
+ */
+export function createAction(action, parameters) {
+ const encodedInput = ethers.utils.defaultAbiCoder.encode(
+ V4_BASE_ACTIONS_ABI_DEFINITION[action].map((v) => v.type),
+ parameters,
+ );
+ return { action, encodedInput };
+}
+
+/**
+ * Adds an action to the action list with proper encoding
+ * @param {number} type - Action type from Actions enum
+ * @param {Array} parameters - Array of parameters for the action
+ * @returns {{newParam: string, newAction: string}} Encoded parameter and action hex
+ */
+export function addAction(type, parameters) {
+ const command = createAction(type, parameters);
+ const newParam = command.encodedInput;
+ const newAction = command.action.toString(16).padStart(2, '0');
+
+ return {
+ newParam,
+ newAction,
+ };
+}
+
+// ============================================================
+// Encoding Helpers
+// ============================================================
+
+/**
+ * Encodes ERC20 approve function call
+ * @param {string} spender - Spender address
+ * @param {ethers.BigNumber|string} amount - Amount to approve
+ * @returns {string} Encoded function call data
+ */
+export function encodeApprove(spender, amount) {
+ const iface = new ethers.utils.Interface([
+ 'function approve(address spender, uint256 amount) returns (bool)',
+ ]);
+ return iface.encodeFunctionData('approve', [spender, amount]);
+}
+
+/**
+ * Encodes Permit2 approve function call
+ * @param {string} token - Token address
+ * @param {string} spender - Spender address
+ * @param {ethers.BigNumber|string} amount - Amount to approve (must fit in uint160)
+ * @param {number} deadline - Expiration timestamp (must fit in uint48)
+ * @returns {string} Encoded function call data
+ */
+export function encodePermit2Approve(token, spender, amount, deadline) {
+ const iface = new ethers.utils.Interface([
+ 'function approve(address token, address spender, uint160 amount, uint48 expiration)',
+ ]);
+
+ // Ensure amount fits in uint160 and deadline fits in uint48
+ const amount160 = ethers.BigNumber.from(amount);
+ const deadline48 = ethers.BigNumber.from(deadline);
+
+ return iface.encodeFunctionData('approve', [
+ token,
+ spender,
+ amount160,
+ deadline48,
+ ]);
+}
diff --git a/src/index.js b/src/index.js
index 77720150..d824880b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -20,6 +20,7 @@ import {
eip747Component,
erc721Component,
swapComparisonComponent,
+ batchUsdcSwapComponent,
} from './components/transactions';
import {
ppomMaliciousSendCalls,
@@ -186,6 +187,7 @@ erc1155Component(transactionsRow);
eip747Component(transactionsRow);
eip5792Component(transactionsRow);
swapComparisonComponent(transactionsRow);
+batchUsdcSwapComponent(transactionsRow);
const ppomSection = document.createElement('section');
mainContainer.appendChild(ppomSection);