From 5ca034c330b8c3ca9c91bc78634015c412e387a9 Mon Sep 17 00:00:00 2001 From: cryptotavares Date: Wed, 5 Nov 2025 19:15:19 +0000 Subject: [PATCH 1/4] feat: add dapp swap comparison button --- src/components/transactions/index.js | 1 + src/components/transactions/swapComparison.js | 259 ++++++++++++++++++ src/index.js | 2 + 3 files changed, 262 insertions(+) create mode 100644 src/components/transactions/swapComparison.js diff --git a/src/components/transactions/index.js b/src/components/transactions/index.js index f885a52a..07d74fd9 100644 --- a/src/components/transactions/index.js +++ b/src/components/transactions/index.js @@ -3,3 +3,4 @@ export * from './erc20'; export * from './erc721'; export * from './erc1155'; export * from './send'; +export * from './swapComparison'; diff --git a/src/components/transactions/swapComparison.js b/src/components/transactions/swapComparison.js new file mode 100644 index 00000000..515faee2 --- /dev/null +++ b/src/components/transactions/swapComparison.js @@ -0,0 +1,259 @@ +import { ethers } from 'ethers'; +import globalContext from '../..'; + +// Constants +const UNIVERSAL_ROUTER = '0x66a9893cc07d91d95644aedd05d03f95e1dba8af'; +const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; +const FEE_RECIPIENT = '0x58c51ee8998e8ef06362df26a0d966bbd0cf5113'; +const ETH_AMOUNT = '0x10a741a462780'; // 0.0003 ETH in hex +const FEE_BIPS = 5000; // 50% in basis points +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' }, + ], +}; + +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', + `
+
+
+

+ Swap Comparison (High Fee Test) +

+ +

+ ⚠️ This swap includes a 50% fee deduction for testing purposes +

+ +
+ Swap Details:
+ • From: 0.0003 ETH
+ • To: USDC
+ • Fee: 50% of output +
+ + + +

+ Status: Not executed +

+ +

+ Transaction Hash: - +

+
+
+
`, + ); + + const swapButton = document.getElementById('swapComparisonSwapButton'); + const statusDisplay = document.getElementById('swapStatus'); + const txHashDisplay = document.getElementById('swapTxHash'); + + document.addEventListener('globalConnectionChange', function (e) { + if (e.detail.connected) { + swapButton.disabled = false; + } + }); + + document.addEventListener('disableAndClear', function () { + swapButton.disabled = true; + }); + + /** + * Build Swap Comparison transaction using working example as template + */ + swapButton.onclick = async () => { + try { + statusDisplay.innerHTML = 'Building transaction...'; + console.log('=== Swap Comparison Transaction ==='); + console.log('Router:', UNIVERSAL_ROUTER); + console.log('ETH Amount:', ETH_AMOUNT, '(0.0003 ETH)'); + console.log('Fee Recipient:', FEE_RECIPIENT); + console.log('Fee %:', FEE_BIPS / 100, '%'); + + // Use working example as template and modify only necessary values + const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes from now + + // Build calldata using template from working example + const data = buildCalldata(deadline, FEE_RECIPIENT, FEE_BIPS); + + statusDisplay.innerHTML = 'Sending transaction...'; + + // Send the transaction + const txParams = { + from: globalContext.accounts[0], + to: UNIVERSAL_ROUTER, + value: ETH_AMOUNT, + data, + }; + + console.log('Transaction params:', txParams); + + const txHash = await globalContext.provider.request({ + method: 'eth_sendTransaction', + params: [txParams], + }); + + console.log('Transaction sent! Hash:', txHash); + statusDisplay.innerHTML = 'Transaction sent!'; + txHashDisplay.innerHTML = txHash; + } catch (error) { + console.error('swap error:', error); + statusDisplay.innerHTML = `Error: ${ + error.message || 'Transaction failed' + }`; + txHashDisplay.innerHTML = '-'; + } + }; +} + +/** + * Build calldata using the working example as a template + * Only modifies: deadline, fee recipient, fee bips, and user address + */ +function buildCalldata(deadline, feeRecipient, feeBips) { + // Working example from the original transaction + // We'll use ethers to properly encode with our values + const commands = '0x10'; // V4_SWAP + + // V4_SWAP input - extracted from working example + // This is the complex nested structure that we keep as-is + const v4SwapInput = buildV4SwapInputFromExample(feeRecipient, feeBips); + + // Encode the execute function call + const inputs = [v4SwapInput]; //, payPortionInput, sweepInput]; + + const iface = new ethers.utils.Interface([ + 'function execute(bytes commands, bytes[] inputs, uint256 deadline)', + ]); + + const calldata = iface.encodeFunctionData('execute', [ + commands, + inputs, + deadline, + ]); + + return calldata; +} + +/** + * Build V4_SWAP input from working example structure + * This uses the exact structure from a known working transaction + */ +function buildV4SwapInputFromExample(feeRecipient, feeBips) { + // From working example: actions = 0x070b0e + let v4Actions = EMPTY_BYTES; + const v4Params = []; + + // Build the 3 params from the working example structure + // These encode the pool configuration and swap parameters + + // Param 1: Pool key and swap amount configuration + // Structure from example: complex nested data for the V4 pool + const poolKey = { + currency0: ethers.constants.AddressZero, // currency0 (ETH) + currency1: USDC_ADDRESS, // currency1 (USDC) + fee: 500, + tickSpacing: 10, + hooks: '0x0000000000000000000000000000000000000000', + }; + const amountOutMinimum = '0x0'; + const swapExactInSingle = addAction(Actions.SWAP_EXACT_IN_SINGLE, [ + { + poolKey, + zeroForOne: true, // The direction of swap is ETH to USDC. Change it to 'false' for the reverse direction + amountIn: ETH_AMOUNT, + amountOutMinimum, // Change according to the slippage desired + hookData: '0x00', + }, + ]); + v4Actions = v4Actions.concat(swapExactInSingle.newAction); + v4Params.push(swapExactInSingle.newParam); + + const settleAll = addAction(Actions.SETTLE_ALL, [ + poolKey.currency0, + ETH_AMOUNT, + ]); + v4Actions = v4Actions.concat(settleAll.newAction); + v4Params.push(settleAll.newParam); + + const takePortion = addAction(Actions.TAKE_PORTION, [ + poolKey.currency1, + feeRecipient, + feeBips, + ]); + v4Actions = v4Actions.concat(takePortion.newAction); + v4Params.push(takePortion.newParam); + + const takeAll = addAction(Actions.TAKE_ALL, [ + poolKey.currency1, + amountOutMinimum, + ]); + v4Actions = v4Actions.concat(takeAll.newAction); + v4Params.push(takeAll.newParam); + + // Encode the V4_SWAP input: (bytes actions, bytes[] params) + const v4SwapInput = ethers.utils.defaultAbiCoder.encode( + ['bytes', 'bytes[]'], + [v4Actions, v4Params], + ); + + return v4SwapInput; +} diff --git a/src/index.js b/src/index.js index 8f7bab13..1e6f2517 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ import { erc1155Component, eip747Component, erc721Component, + swapComparisonComponent, } from './components/transactions'; import { ppomMaliciousSendCalls, @@ -184,6 +185,7 @@ erc721Component(transactionsRow); erc1155Component(transactionsRow); eip747Component(transactionsRow); eip5792Component(transactionsRow); +swapComparisonComponent(transactionsRow); const ppomSection = document.createElement('section'); mainContainer.appendChild(ppomSection); From c2e0fd80bd95c68d9d0d14fe109b7da48b83b386 Mon Sep 17 00:00:00 2001 From: cryptotavares Date: Thu, 6 Nov 2025 17:45:13 +0000 Subject: [PATCH 2/4] chore: add inputs for fee, recipient and amount --- src/components/transactions/swapComparison.js | 222 ++++++++++++++++-- 1 file changed, 203 insertions(+), 19 deletions(-) diff --git a/src/components/transactions/swapComparison.js b/src/components/transactions/swapComparison.js index 515faee2..7690fcc7 100644 --- a/src/components/transactions/swapComparison.js +++ b/src/components/transactions/swapComparison.js @@ -4,9 +4,9 @@ import globalContext from '../..'; // Constants const UNIVERSAL_ROUTER = '0x66a9893cc07d91d95644aedd05d03f95e1dba8af'; const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; -const FEE_RECIPIENT = '0x58c51ee8998e8ef06362df26a0d966bbd0cf5113'; -const ETH_AMOUNT = '0x10a741a462780'; // 0.0003 ETH in hex -const FEE_BIPS = 5000; // 50% in basis points +const DEFAULT_FEE_RECIPIENT = '0x58c51ee8998e8ef06362df26a0d966bbd0cf5113'; +const DEFAULT_ETH_AMOUNT = '0.0003'; // 0.0003 ETH +const DEFAULT_FEE_PERCENTAGE = 50; // 50% const EMPTY_BYTES = '0x'; const Actions = { @@ -42,6 +42,83 @@ const V4_BASE_ACTIONS_ABI_DEFINITION = { ], }; +// Helper functions for input validation and parsing +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 + return 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; + } +} + +function getConfigValues() { + const feeRecipientElement = document.getElementById('feeRecipientInput'); + const ethAmountElement = document.getElementById('ethAmountInput'); + const feePercentageElement = document.getElementById('feePercentageInput'); + + const feeRecipientInput = feeRecipientElement + ? feeRecipientElement.value.trim() + : ''; + const ethAmountInput = ethAmountElement ? ethAmountElement.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 ethAmountHex = + parseEthAmount(ethAmountInput) || + ethers.utils.parseEther(DEFAULT_ETH_AMOUNT).toHexString(); + + const feeBips = + parseFeePercentage(feePercentageInput) || DEFAULT_FEE_PERCENTAGE * 100; + + return { + feeRecipient, + ethAmountHex, + feeBips, + ethAmountDisplay: ethAmountInput || DEFAULT_ETH_AMOUNT, + feePercentageDisplay: + feePercentageInput || DEFAULT_FEE_PERCENTAGE.toString(), + }; +} + function createAction(action, parameters) { const encodedInput = ethers.utils.defaultAbiCoder.encode( V4_BASE_ACTIONS_ABI_DEFINITION[action].map((v) => v.type), @@ -72,14 +149,79 @@ export function swapComparisonComponent(parentContainer) {

- ⚠️ This swap includes a 50% fee deduction for testing purposes + ⚠️ This swap includes a high fee for testing purposes

- Swap Details:
- • From: 0.0003 ETH
+ Current Swap Details:
+ + • From: ${DEFAULT_ETH_AMOUNT} ETH
• To: USDC
- • Fee: 50% of output + • Fee: ${DEFAULT_FEE_PERCENTAGE}% of output +
+
+ + +
+
+
+ Configuration (Optional) +
+ +
+ + + Leave empty for default (${DEFAULT_ETH_AMOUNT} ETH) +
+ +
+ + + Leave empty for default (${DEFAULT_FEE_PERCENTAGE}%) +
+ +
+ + + Leave empty for default address +
+ + +

@@ -105,6 +247,31 @@ export function swapComparisonComponent(parentContainer) { const swapButton = document.getElementById('swapComparisonSwapButton'); const statusDisplay = document.getElementById('swapStatus'); const txHashDisplay = document.getElementById('swapTxHash'); + const resetButton = document.getElementById('resetConfigButton'); + const ethAmountInput = document.getElementById('ethAmountInput'); + const feePercentageInput = document.getElementById('feePercentageInput'); + const feeRecipientInput = document.getElementById('feeRecipientInput'); + const displayEthAmount = document.getElementById('displayEthAmount'); + const displayFeePercentage = document.getElementById('displayFeePercentage'); + + // Function to update the swap details display + function updateSwapDetailsDisplay() { + const config = getConfigValues(); + displayEthAmount.textContent = config.ethAmountDisplay; + displayFeePercentage.textContent = config.feePercentageDisplay; + } + + // Add event listeners for input changes to update display + ethAmountInput.addEventListener('input', updateSwapDetailsDisplay); + feePercentageInput.addEventListener('input', updateSwapDetailsDisplay); + + // Reset button handler + resetButton.addEventListener('click', function () { + ethAmountInput.value = ''; + feePercentageInput.value = ''; + feeRecipientInput.value = ''; + updateSwapDetailsDisplay(); + }); document.addEventListener('globalConnectionChange', function (e) { if (e.detail.connected) { @@ -122,17 +289,30 @@ export function swapComparisonComponent(parentContainer) { swapButton.onclick = async () => { try { statusDisplay.innerHTML = 'Building transaction...'; + + // Get configuration values from inputs or use defaults + const config = getConfigValues(); + console.log('=== Swap Comparison Transaction ==='); console.log('Router:', UNIVERSAL_ROUTER); - console.log('ETH Amount:', ETH_AMOUNT, '(0.0003 ETH)'); - console.log('Fee Recipient:', FEE_RECIPIENT); - console.log('Fee %:', FEE_BIPS / 100, '%'); + console.log( + 'ETH Amount:', + config.ethAmountHex, + `(${config.ethAmountDisplay} ETH)`, + ); + console.log('Fee Recipient:', config.feeRecipient); + console.log('Fee %:', config.feeBips / 100, '%'); // Use working example as template and modify only necessary values const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes from now // Build calldata using template from working example - const data = buildCalldata(deadline, FEE_RECIPIENT, FEE_BIPS); + const data = buildCalldata( + deadline, + config.feeRecipient, + config.feeBips, + config.ethAmountHex, + ); statusDisplay.innerHTML = 'Sending transaction...'; @@ -140,7 +320,7 @@ export function swapComparisonComponent(parentContainer) { const txParams = { from: globalContext.accounts[0], to: UNIVERSAL_ROUTER, - value: ETH_AMOUNT, + value: config.ethAmountHex, data, }; @@ -166,16 +346,20 @@ export function swapComparisonComponent(parentContainer) { /** * Build calldata using the working example as a template - * Only modifies: deadline, fee recipient, fee bips, and user address + * Only modifies: deadline, fee recipient, fee bips, eth amount, and user address */ -function buildCalldata(deadline, feeRecipient, feeBips) { +function buildCalldata(deadline, feeRecipient, feeBips, ethAmount) { // Working example from the original transaction // We'll use ethers to properly encode with our values const commands = '0x10'; // V4_SWAP // V4_SWAP input - extracted from working example // This is the complex nested structure that we keep as-is - const v4SwapInput = buildV4SwapInputFromExample(feeRecipient, feeBips); + const v4SwapInput = buildV4SwapInputFromExample( + feeRecipient, + feeBips, + ethAmount, + ); // Encode the execute function call const inputs = [v4SwapInput]; //, payPortionInput, sweepInput]; @@ -197,7 +381,7 @@ function buildCalldata(deadline, feeRecipient, feeBips) { * Build V4_SWAP input from working example structure * This uses the exact structure from a known working transaction */ -function buildV4SwapInputFromExample(feeRecipient, feeBips) { +function buildV4SwapInputFromExample(feeRecipient, feeBips, ethAmount) { // From working example: actions = 0x070b0e let v4Actions = EMPTY_BYTES; const v4Params = []; @@ -219,7 +403,7 @@ function buildV4SwapInputFromExample(feeRecipient, feeBips) { { poolKey, zeroForOne: true, // The direction of swap is ETH to USDC. Change it to 'false' for the reverse direction - amountIn: ETH_AMOUNT, + amountIn: ethAmount, amountOutMinimum, // Change according to the slippage desired hookData: '0x00', }, @@ -229,7 +413,7 @@ function buildV4SwapInputFromExample(feeRecipient, feeBips) { const settleAll = addAction(Actions.SETTLE_ALL, [ poolKey.currency0, - ETH_AMOUNT, + ethAmount, ]); v4Actions = v4Actions.concat(settleAll.newAction); v4Params.push(settleAll.newParam); From c98f2cbc9fca0f1507cc3204bf60d1461bace4a6 Mon Sep 17 00:00:00 2001 From: cryptotavares Date: Fri, 7 Nov 2025 10:34:50 +0000 Subject: [PATCH 3/4] chore: add disclaimer for mainnet only --- src/components/transactions/swapComparison.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/transactions/swapComparison.js b/src/components/transactions/swapComparison.js index 7690fcc7..e45afae6 100644 --- a/src/components/transactions/swapComparison.js +++ b/src/components/transactions/swapComparison.js @@ -145,7 +145,7 @@ export function swapComparisonComponent(parentContainer) {

- Swap Comparison (High Fee Test) + Swap Comparison (High Fee Test - Mainnet only)

From 138ec5f063585b0530073765602f5071ee4e8f7b Mon Sep 17 00:00:00 2001 From: cryptotavares Date: Fri, 7 Nov 2025 11:53:17 +0000 Subject: [PATCH 4/4] chore: disable swap button if not on mainnet --- src/components/transactions/swapComparison.js | 6 +++++- src/index.js | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/transactions/swapComparison.js b/src/components/transactions/swapComparison.js index e45afae6..9f8d09e7 100644 --- a/src/components/transactions/swapComparison.js +++ b/src/components/transactions/swapComparison.js @@ -273,8 +273,12 @@ export function swapComparisonComponent(parentContainer) { updateSwapDetailsDisplay(); }); + document.addEventListener('newChainIdInt', function (e) { + swapButton.disabled = e.detail.chainIdInt !== 1; + }); + document.addEventListener('globalConnectionChange', function (e) { - if (e.detail.connected) { + if (e.detail.connected && globalContext.chainIdInt === 1) { swapButton.disabled = false; } }); diff --git a/src/index.js b/src/index.js index 1e6f2517..77720150 100644 --- a/src/index.js +++ b/src/index.js @@ -441,6 +441,12 @@ const handleNewChain = (chainId) => { if (!scrollToHandled) { handleScrollTo({ delay: true }); } + + const changeEvent = new CustomEvent('newChainIdInt', { + detail: { chainIdInt: globalContext.chainIdInt }, + }); + document.dispatchEvent(changeEvent); + updateCurrentNetworkDisplay(); updateActiveNetworkInModal(); };