Skip to content

Commit

Permalink
Estimate txs in bundles to support airdrop claims (irfanbozkurt#21)
Browse files Browse the repository at this point in the history
* estimate bundles for airdrop claims

* only use bundle simulation on production

* update goerli relay to match new flow
  • Loading branch information
escottalexander authored Feb 17, 2024
1 parent 8653d4d commit 399af69
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 206 deletions.
2 changes: 1 addition & 1 deletion packages/nextjs/components/CustomPortal/CustomPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const CustomPortal = ({ indicator, title, image, children, video, descrip
<h3 className={`${styles.title}`}>{title}</h3>
<div>
{!!image ? <Image className={`${styles.image}`} src={image} alt={""} /> : <></>}
{!!indicator ? <div className={styles.indicator}>{indicator > 0 ? indicator : 11} BLOCKS</div> : <></>}
{!!indicator ? <div className={styles.indicator}>Attempting in block #{indicator}</div> : <></>}
</div>

{!!video ? <Image className={`${styles.image}`} src={video} alt={""} /> : <></>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
.image {
height: 190px;
width: 190px;
margin: auto;
}
.indicator {
text-align: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ interface IProps {
hackedAddress: string;
donationValue: string;
setDonationValue: (amt: string) => void;
blockCountdown: number;
attemptedBlock: number;
isDonationLoading: boolean;
totalGasEstimate: BigNumber;
rpcParams: IRPCParams | undefined;
Expand All @@ -60,7 +60,7 @@ export const RecoveryProcess = ({
signTransactionsStep,
finishProcess,
showTipsModal,
blockCountdown,
attemptedBlock,
donationValue,
setDonationValue,
connectedAddress,
Expand Down Expand Up @@ -215,7 +215,7 @@ export const RecoveryProcess = ({
<CustomPortal
title={"Sign each transaction"}
description={
"Now you will be prompted to sign a transaction for each asset you selected for recovery. Approve each transaction as it appears."
"Now you will be prompted to sign a transaction for each asset you selected for recovery. Approve each transaction as it appears. Ignore pending transaction warnings."
}
image={MultiSignSvg}
/>
Expand All @@ -227,28 +227,30 @@ export const RecoveryProcess = ({
<CustomPortal
title={"Wait for confirmation"}
description={
"Your asset recovery is in progress. Stay on this page and wait for your transactions to be included in a block."
"Your asset recovery is in progress. Stay on this page and wait while we attempt to include your transactions in a block."
}
image={ClockSvg}
indicator={blockCountdown}
indicator={attemptedBlock}
/>
);
}

if (recoveryStatus == RecoveryProcessStatus.SUCCESS) {
let actions:any[] =[ {
text: "Finish",
disabled: false,
isSecondary: false,
action: () => finishProcess(),
}]
if(showDonationsButton){
actions.unshift( {
let actions: any[] = [
{
text: "Finish",
disabled: false,
isSecondary: false,
action: () => finishProcess(),
},
];
if (showDonationsButton) {
actions.unshift({
text: "Donate",
isSecondary: true,
disabled: false,
action: () => showTipsModal(),
})
});
}
return (
<CustomPortal
Expand Down Expand Up @@ -279,35 +281,41 @@ export const RecoveryProcess = ({
);
}
if (recoveryStatus === RecoveryProcessStatus.DONATE) {
return(
return (
<CustomPortal
title={"Support Our Mission"}
description={
"Your contribution will help us continue to offer this service free of charge. Thank you for your support!"
}
buttons={[
{
text: isDonationLoading ? "Sending..." : "Donate",
disabled: isDonationLoading || !hasEnoughEth || !address ,
action: () => finishProcess(),
},
]}
image={TipsSvg}
>

<>
<div className={styles.inputContainer}>
<label className={styles.label} htmlFor="tip">
Tip
</label>
<div className="mt-2" />
<InputBase name="tip" placeholder="0.0" value={donationValue} onChange={(val) => setDonationValue(val.replace(",", "."))} />
<span className={`${styles.eth} text-base-100`}>ETH</span>
</div>
<p className={`text-secondary-content`}>Please change the network first to <b>{networkName}</b></p>
</>
</CustomPortal>
)
title={"Support Our Mission"}
description={
"Your contribution will help us continue to offer this service free of charge. Thank you for your support!"
}
buttons={[
{
text: isDonationLoading ? "Sending..." : "Donate",
disabled: isDonationLoading || !hasEnoughEth || !address,
action: () => finishProcess(),
},
]}
image={TipsSvg}
>
<>
<div className={styles.inputContainer}>
<label className={styles.label} htmlFor="tip">
Tip
</label>
<div className="mt-2" />
<InputBase
name="tip"
placeholder="0.0"
value={donationValue}
onChange={val => setDonationValue(val.replace(",", "."))}
/>
<span className={`${styles.eth} text-base-100`}>ETH</span>
</div>
<p className={`text-secondary-content`}>
Please change the network first to <b>{networkName}</b>
</p>
</>
</CustomPortal>
);
}

return <></>;
Expand Down
84 changes: 56 additions & 28 deletions packages/nextjs/hooks/flashbotRecoveryBundle/useGasEstimation.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,80 @@
import { useState } from "react";
import { useShowError } from "./useShowError";
import { FlashbotsBundleProvider } from "@flashbots/ethers-provider-bundle";
import { Alchemy, DebugTransaction, Network } from "alchemy-sdk";
import { BigNumber } from "ethers";
import { parseEther } from "viem";
import { usePublicClient } from "wagmi";
import { CoreTxToSign, RecoveryTx } from "~~/types/business";
import { BLOCKS_IN_THE_FUTURE } from "~~/utils/constants";
import { getTargetNetwork } from "~~/utils/scaffold-eth";

const alchemyApiKey = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY;

export const useGasEstimation = () => {
const targetNetwork = getTargetNetwork();
const publicClient = usePublicClient({ chainId: targetNetwork.id });
const [alchemy] = useState<Alchemy>(
new Alchemy({
apiKey: alchemyApiKey,
network: targetNetwork.network == "goerli" ? Network.ETH_GOERLI : Network.ETH_MAINNET,
}),
);
const { showError } = useShowError();
const estimateTotalGasPrice = async (
txs: RecoveryTx[],
deleteTransaction: (id: number) => void,
modifyTransactions: (txs: RecoveryTx[]) => void,
) => {
try {
const 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());
}),
);
let estimates: BigNumber[] = [];
if (txs.length <= 3 && targetNetwork.network == "mainnet") {
// 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);
});
} 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());
}),
);
}
const maxBaseFeeInFuture = await maxBaseFeeInFutureBlock();
const priorityFee = BigNumber.from(10).pow(10);
// Priority fee is 3 gwei
const priorityFee = BigNumber.from(3).mul(1e9);
const txsWithGas: RecoveryTx[] = estimates.map((gas, index) => {
const mainTx = txs[index] as RecoveryTx;
// If the tx doesn't exist, skip adding gas properties
if (!mainTx) return mainTx;
const tx = Object.assign({}, mainTx.toEstimate) as CoreTxToSign;
// Buffer the gas limit by 5%
tx.gas = gas.mul(105).div(100).toString();
// Buffer the gas limit by 15%
tx.gas = gas.mul(115).div(100).toString();
// Set type
tx.type = "eip1559";
// Set suggested gas properties
Expand All @@ -59,7 +89,7 @@ export const useGasEstimation = () => {
}
const totalGasCost = estimates
.reduce((acc: BigNumber, val: BigNumber) => acc.add(val), BigNumber.from("0"))
.mul(105)
.mul(115)
.div(100);
const gasPrice = maxBaseFeeInFuture.add(priorityFee);
return totalGasCost.mul(gasPrice);
Expand All @@ -75,10 +105,8 @@ export const useGasEstimation = () => {
const maxBaseFeeInFutureBlock = async () => {
const blockNumberNow = await publicClient.getBlockNumber();
const block = await publicClient.getBlock({ blockNumber: blockNumberNow });
return FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(
BigNumber.from(block.baseFeePerGas),
BLOCKS_IN_THE_FUTURE[targetNetwork.id],
);
// 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 {
Expand Down
Loading

0 comments on commit 399af69

Please sign in to comment.