From 203d3b172adadd5621010a5c99e03e80bcde5207 Mon Sep 17 00:00:00 2001 From: Eric Bishard Date: Sun, 11 May 2025 05:23:08 -0400 Subject: [PATCH] feat(transactions): Add checkForPotentialDuplicates method for transaction validation This change adds a new public method to the SmartTransactionsController that checks if a transaction would likely fail by: 1. Checking for pending transactions with matching parameters 2. Simulating the transaction to detect potential reverts 3. Extracting user-friendly error messages from revert reasons The method is designed to prevent duplicate or failing transactions from being submitted to the blockchain and works in conjunction with changes to the MetaMask extension's SmartTransactionHook. --- src/SmartTransactionsController.ts | 140 +++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/src/SmartTransactionsController.ts b/src/SmartTransactionsController.ts index 3615978..ce31058 100644 --- a/src/SmartTransactionsController.ts +++ b/src/SmartTransactionsController.ts @@ -836,6 +836,146 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo }; } +/** + * Extracts a readable revert reason from an error message + * + * @param errorMessage - The full error message + * @returns The extracted revert reason or null if not found + * @private + */ +#extractRevertReason(errorMessage: string): string | null { + // Common patterns in revert messages + const patterns = [ + /execution reverted: (.*?)($|\s|\.)/i, + /reason string "(.*?)"/i, + /revert: (.*?)($|\s|\.)/i, + /revert with reason "(.*?)"/i, + /already claimed/i, // Special case for our specific scenario + ]; + + for (const pattern of patterns) { + const match = errorMessage.match(pattern); + if (match) { + // For the "already claimed" special case or similar + if (pattern.toString().includes('already claimed')) { + return 'Already claimed'; + } + return match[1]; + } + } + + return null; +} + +/** + * Checks if there are any pending duplicate transactions that would likely fail + * + * @param params - Parameters to check for duplicates + * @param params.from - The address sending the transaction + * @param params.to - The address receiving the transaction + * @param params.data - The transaction data (function call and parameters) + * @param params.value - The transaction value in wei (optional) + * @param params.chainId - The chain ID (used indirectly through EthQuery) + * @param params.networkClientId - The network client ID (optional) + * @returns An object indicating if duplicates were found and the reason + */ +async checkForPotentialDuplicates({ + from, + to, + data, + value, + chainId, + networkClientId, +}: { + from: string; + to: string; + data: string; + value?: string; + chainId: Hex; + networkClientId?: NetworkClientId; +}): Promise<{ hasDuplicates: boolean; reason?: string }> { + // log at start of function + console.log('checkForPotentialDuplicates called with', { + from: from?.substring(0, 10), + to: to?.substring(0, 10), + data: data?.substring(0, 10), + chainId + }); + + // Normalize addresses for comparison + const normalizedFrom = from.toLowerCase(); + const normalizedTo = to.toLowerCase(); + + // 1. Check for pending local transactions with matching parameters + const currentSmartTransactions = this.#getCurrentSmartTransactions(); + + // log for current transactions + console.log('Current smart transactions count:', currentSmartTransactions.length); + + const pendingDuplicates = currentSmartTransactions.filter( + (tx) => + isSmartTransactionPending(tx) && + tx.txParams?.from?.toLowerCase() === normalizedFrom && + tx.txParams?.to?.toLowerCase() === normalizedTo && + tx.txParams?.data === data + ); + + // log for pending duplicates + console.log('Pending duplicates found:', pendingDuplicates.length); + + if (pendingDuplicates.length > 0) { + const result = { + hasDuplicates: true, + reason: 'Similar transaction is already pending' + }; + console.log('checkForPotentialDuplicates result (from pending):', result); + return result; + } + + // 2. Optional transaction simulation to detect potential reverts + // Only do this if we have a provider available + try { + console.log('About to get ethQuery and simulate transaction'); + const ethQuery = this.#getEthQuery({ networkClientId }); + + // Use eth_call to simulate the transaction + console.log('Simulating transaction with eth_call'); + await query(ethQuery, 'call', [{ + from: normalizedFrom, + to: normalizedTo, + data, + value: value || '0x0' + }, 'latest']); + + // If we get here, the simulation was successful + const result = { hasDuplicates: false }; + console.log('checkForPotentialDuplicates result (no issues):', result); + return result; + } catch (error) { + // If the simulation fails, it suggests the transaction would revert + // Extract a user-friendly error message + console.log('Transaction simulation failed with error:', error); + let errorMessage = 'Transaction simulation failed'; + + if (error instanceof Error) { + // Try to extract a reason from the error message + const revertReason = this.#extractRevertReason(error.message); + if (revertReason) { + errorMessage = `Transaction would fail: ${revertReason}`; + } else { + errorMessage = `Transaction would fail: ${error.message}`; + } + } + + const result = { + hasDuplicates: true, + reason: errorMessage + }; + console.log('checkForPotentialDuplicates result (from simulation):', result); + return result; + } +} + clearFees(): Fees { const fees = { approvalTxFees: null,