From eaa659239774adb8111d1d32229c005651e1d002 Mon Sep 17 00:00:00 2001 From: cryptotavares Date: Tue, 18 Nov 2025 00:16:34 +0000 Subject: [PATCH] chore: add batch approve and swap --- src/components/transactions/batchUsdcSwap.js | 423 ++++++++++++++++++ src/components/transactions/index.js | 1 + src/components/transactions/swapComparison.js | 127 +----- src/components/transactions/swapUtils.js | 258 +++++++++++ src/index.js | 2 + 5 files changed, 700 insertions(+), 111 deletions(-) create mode 100644 src/components/transactions/batchUsdcSwap.js create mode 100644 src/components/transactions/swapUtils.js 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);