diff --git a/packages/nextjs/components/Processes/BundlingProcess/Steps/TransactionBundleStep/TransactionBundleStep.tsx b/packages/nextjs/components/Processes/BundlingProcess/Steps/TransactionBundleStep/TransactionBundleStep.tsx index a0fbd1c..515352c 100644 --- a/packages/nextjs/components/Processes/BundlingProcess/Steps/TransactionBundleStep/TransactionBundleStep.tsx +++ b/packages/nextjs/components/Processes/BundlingProcess/Steps/TransactionBundleStep/TransactionBundleStep.tsx @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction, useEffect } from "react"; +import { Dispatch, SetStateAction, useCallback, useEffect } from "react"; import Image from "next/image"; import GasSvg from "../../../../../public/assets/flashbotRecovery/gas.svg"; import styles from "./transactionBundleStep.module.css"; @@ -34,22 +34,17 @@ export const TransactionBundleStep = ({ }: IProps) => { const { estimateTotalGasPrice } = useGasEstimation(); + const updateGasEstimate = useCallback(async () => { + if (transactions.length === 0) return; + const estimate = await estimateTotalGasPrice(transactions, removeUnsignedTx, modifyTransactions); + setTotalGasEstimate(estimate); + }, [transactions, estimateTotalGasPrice, setTotalGasEstimate]); + useEffect(() => { - if (transactions.length == 0) { - return; - } - estimateTotalGasPrice(transactions, removeUnsignedTx, modifyTransactions).then(setTotalGasEstimate); - }, [transactions.length]); + updateGasEstimate(); + }, [updateGasEstimate]); - useInterval(() => { - if (transactions.length == 0) { - return; - } - const updateTotalGasEstimate = async () => { - setTotalGasEstimate(await estimateTotalGasPrice(transactions, removeUnsignedTx, modifyTransactions)); - }; - updateTotalGasEstimate(); - }, 5000); + useInterval(updateGasEstimate, transactions.length > 0 ? 10000 : null); const removeUnsignedTx = (txId: number) => { modifyTransactions((prev: RecoveryTx[]) => { diff --git a/packages/nextjs/hooks/flashbotRecoveryBundle/useGasEstimation.ts b/packages/nextjs/hooks/flashbotRecoveryBundle/useGasEstimation.ts index 3230776..fb83fa9 100644 --- a/packages/nextjs/hooks/flashbotRecoveryBundle/useGasEstimation.ts +++ b/packages/nextjs/hooks/flashbotRecoveryBundle/useGasEstimation.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { useShowError } from "./useShowError"; import { FlashbotsBundleProvider } from "@flashbots/ethers-provider-bundle"; import { Alchemy, DebugTransaction, Network } from "alchemy-sdk"; @@ -20,51 +20,88 @@ export const useGasEstimation = () => { }), ); const { showError } = useShowError(); + + // Cache for gas estimates and bundle hash + const lastBundleHash = useRef(""); + const lastEstimates = useRef([]); + const lastBaseFeeTimestamp = useRef(0); + const lastBaseFee = useRef(BigNumber.from(0)); + + // Function to generate a hash for the transaction bundle + const getBundleHash = useCallback((txs: RecoveryTx[]): string => { + return txs.map(tx => tx.toEstimate?.data || "").join(""); + }, []); + + const maxBaseFeeInFutureBlock = async () => { + const now = Date.now(); + // Cache base fee for 1 block (~12 seconds) + if (now - lastBaseFeeTimestamp.current < 12000 && !lastBaseFee.current.isZero()) { + return lastBaseFee.current; + } + + const block = await publicClient.getBlock(); + const maxBaseFee = FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(BigNumber.from(block.baseFeePerGas), 3); + + lastBaseFee.current = maxBaseFee; + lastBaseFeeTimestamp.current = now; + return maxBaseFee; + }; + const estimateTotalGasPrice = async ( txs: RecoveryTx[], deleteTransaction: (id: number) => void, modifyTransactions: (txs: RecoveryTx[]) => void, ) => { try { + const currentBundleHash = getBundleHash(txs); let estimates: BigNumber[] = []; - if (txs.length <= 3 && targetNetwork.network != "sepolia") { - // Try to estimate the gas for the entire bundle - const bundle = [...txs.map(tx => tx.toEstimate)]; - // TODO: Add catching so that if the bundle hasn't changed we don't need to call Alchemy again - const simulation = await alchemy.transact.simulateExecutionBundle(bundle as DebugTransaction[]); - estimates = simulation.map((result, index) => { - if (result.calls[0].error) { - console.warn( - `Following tx will fail when bundle is submitted, so it's removed from the bundle right now. The contract might be a hacky one, and you can try further manipulation via crafting a custom call.`, - ); - console.warn(index, result); - deleteTransaction(index); - return BigNumber.from("0"); - } - return BigNumber.from(result.calls[0].gasUsed); - }); + + // Check if we can use cached estimates + if (currentBundleHash === lastBundleHash.current && lastEstimates.current.length === txs.length) { + estimates = lastEstimates.current; } else { - // Estimate each transaction individually - estimates = await Promise.all( - txs - .filter(a => a) - .map(async (tx, txId) => { - const { to, from, data, value = "0" } = tx.toEstimate; - const estimate = await publicClient - .estimateGas({ account: from, to, data, value: parseEther(value) }) - .catch(e => { - console.warn( - `Following tx will fail when bundle is submitted, so it's removed from the bundle right now. The contract might be a hacky one, and you can try further manipulation via crafting a custom call.`, - ); - console.warn(tx); - console.warn(e); - deleteTransaction(txId); - return BigNumber.from("0"); - }); - return BigNumber.from(estimate.toString()); - }), - ); + if (txs.length <= 3 && targetNetwork.network != "sepolia") { + // Try to estimate the gas for the entire bundle + const bundle = [...txs.map(tx => tx.toEstimate)]; + const simulation = await alchemy.transact.simulateExecutionBundle(bundle as DebugTransaction[]); + estimates = simulation.map((result, index) => { + if (result.calls[0].error) { + console.warn( + `Following tx will fail when bundle is submitted, so it's removed from the bundle right now. The contract might be a hacky one, and you can try further manipulation via crafting a custom call.`, + ); + console.warn(index, result); + deleteTransaction(index); + return BigNumber.from("0"); + } + return BigNumber.from(result.calls[0].gasUsed); + }); + } else { + // Estimate each transaction individually + estimates = await Promise.all( + txs + .filter(a => a) + .map(async (tx, txId) => { + const { to, from, data, value = "0" } = tx.toEstimate; + const estimate = await publicClient + .estimateGas({ account: from, to, data, value: parseEther(value) }) + .catch(e => { + console.warn( + `Following tx will fail when bundle is submitted, so it's removed from the bundle right now. The contract might be a hacky one, and you can try further manipulation via crafting a custom call.`, + ); + console.warn(tx); + console.warn(e); + deleteTransaction(txId); + return BigNumber.from("0"); + }); + return BigNumber.from(estimate.toString()); + }), + ); + } + // Cache the new estimates and bundle hash + lastEstimates.current = estimates; + lastBundleHash.current = currentBundleHash; } + const maxBaseFeeInFuture = await maxBaseFeeInFutureBlock(); // Priority fee is 3 gwei const priorityFee = BigNumber.from(3).mul(1e9); @@ -102,13 +139,6 @@ export const useGasEstimation = () => { } }; - const maxBaseFeeInFutureBlock = async () => { - const blockNumberNow = await publicClient.getBlockNumber(); - const block = await publicClient.getBlock({ blockNumber: blockNumberNow }); - // Get the max base fee in 3 blocks to reduce the amount of eth spent on the transaction (possible to get priced out if blocks are full but unlikely) - return FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(BigNumber.from(block.baseFeePerGas), 3); - }; - return { estimateTotalGasPrice, }; diff --git a/packages/nextjs/scaffold.config.ts b/packages/nextjs/scaffold.config.ts index d87dd38..426888d 100644 --- a/packages/nextjs/scaffold.config.ts +++ b/packages/nextjs/scaffold.config.ts @@ -22,10 +22,10 @@ const scaffoldConfig = { // You can get your own at https://dashboard.alchemyapi.io // It's recommended to store it in an env variable: // .env.local for local testing, and in the Vercel/system env config for live apps. - alchemyApiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF", + alchemyApiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || "", // Extra Alchemy key for handling lower tier requests (blockheight, etc) - alchemyApiKey2: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY_2 || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF", + alchemyApiKey2: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY_2 || "", // This is ours WalletConnect's default project ID. // You can get your own at https://cloud.walletconnect.com