Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/cache in useGasEstimation #67

Merged
merged 3 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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[]) => {
Expand Down
118 changes: 74 additions & 44 deletions packages/nextjs/hooks/flashbotRecoveryBundle/useGasEstimation.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -20,51 +20,88 @@ export const useGasEstimation = () => {
}),
);
const { showError } = useShowError();

// Cache for gas estimates and bundle hash
const lastBundleHash = useRef<string>("");
const lastEstimates = useRef<BigNumber[]>([]);
const lastBaseFeeTimestamp = useRef<number>(0);
const lastBaseFee = useRef<BigNumber>(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()) {
escottalexander marked this conversation as resolved.
Show resolved Hide resolved
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;
escottalexander marked this conversation as resolved.
Show resolved Hide resolved
} 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);
Expand Down Expand Up @@ -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,
};
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/scaffold.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down