diff --git a/__tests__/unit/transactions.spec.ts b/__tests__/unit/transactions.spec.ts index 436b3bf..95b0b5f 100644 --- a/__tests__/unit/transactions.spec.ts +++ b/__tests__/unit/transactions.spec.ts @@ -37,17 +37,17 @@ describe('transactions', () => { const expectedError = new Error('Missing property baseFeePerGas on block'); try { - gasModule.getMainnetGasType2Parameters({ block: emptyBlock, burstSize: 2, priorityFeeInWei }); + gasModule.getMainnetGasType2Parameters({ block: emptyBlock, blocksAhead: 2, priorityFeeInWei }); } catch (err) { expect(err).toStrictEqual(expectedError); } }); it('should call getBaseFeeInNextBlock when blocksAhead is 0', async () => { - const burstSize = 0; + const blocksAhead = 0; when(FlashbotsBundleProvider.getBaseFeeInNextBlock).mockReturnValue(mockFlashbotsResponse); - gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, burstSize, priorityFeeInWei }); + gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, blocksAhead, priorityFeeInWei }); expect(FlashbotsBundleProvider.getBaseFeeInNextBlock).toHaveBeenCalledWith( FAKE_BLOCK.baseFeePerGas, @@ -57,10 +57,10 @@ describe('transactions', () => { }); it('should call getBaseFeeInNextBlock when blocksAhead is 1', async () => { - const burstSize = 1; + const blocksAhead = 1; when(FlashbotsBundleProvider.getBaseFeeInNextBlock).mockReturnValue(mockFlashbotsResponse); - gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, burstSize, priorityFeeInWei }); + gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, blocksAhead, priorityFeeInWei }); expect(FlashbotsBundleProvider.getBaseFeeInNextBlock).toHaveBeenCalledWith( FAKE_BLOCK.baseFeePerGas, @@ -70,12 +70,12 @@ describe('transactions', () => { }); it('should call getMaxBaseFeeInFutureBlock when blocksAhead is more than 1', async () => { - const burstSize = 2; + const blocksAhead = 2; when(FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock).mockReturnValue(mockFlashbotsResponse); - gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, burstSize, priorityFeeInWei }); + gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, blocksAhead, priorityFeeInWei }); - expect(FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock).toHaveBeenCalledWith(FAKE_BLOCK.baseFeePerGas, burstSize); + expect(FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock).toHaveBeenCalledWith(FAKE_BLOCK.baseFeePerGas, blocksAhead); }); it('should return the right maxFeePerGas', async () => { @@ -85,7 +85,7 @@ describe('transactions', () => { const maxFeePerGas = BigNumber.from(priorityFeeInWei).add(mockFlashbotsResponse); - const fnCall = gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, burstSize: 2, priorityFeeInWei }); + const fnCall = gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, blocksAhead: 2, priorityFeeInWei }); expect(fnCall.maxFeePerGas).toEqual(maxFeePerGas); }); @@ -95,7 +95,7 @@ describe('transactions', () => { when(FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock).mockReturnValue(mockFlashbotsResponse); const maxFeePerGas = BigNumber.from(priorityFeeInWei).add(mockFlashbotsResponse); - expect(gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, burstSize: 2, priorityFeeInWei })).toEqual({ + expect(gasModule.getMainnetGasType2Parameters({ block: FAKE_BLOCK, blocksAhead: 2, priorityFeeInWei })).toEqual({ priorityFee: BigNumber.from(priorityFeeInWei), maxFeePerGas, }); diff --git a/package.json b/package.json index 0de0241..47c97bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@keep3r-network/keeper-scripting-utils", - "version": "1.1.0", + "version": "1.1.1", "description": "A library containing helper functions that facilitate scripting for keepers of the Keep3r Network", "keywords": [ "ethereum", diff --git a/src/broadcastors/flashbotsBroadcastor.ts b/src/broadcastors/flashbotsBroadcastor.ts deleted file mode 100644 index 06fb2c7..0000000 --- a/src/broadcastors/flashbotsBroadcastor.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { getGasParametersNextBlock, populateTx, sendAndHandleResponse } from '../transactions'; -import type { TransactionRequest } from '@ethersproject/abstract-provider'; -import type { FlashbotsBundleTransaction, FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle'; -import type { Wallet, Overrides } from 'ethers'; -import { BroadcastorProps } from 'types'; - -/** - * @notice Creates and populate a transaction for work in a determinated job using flashbots - * - * @param flashbotsProvider The flashbot provider that will send the bundle - * @param priorityFeeInWei The priority fee in wei - * @param gasLimit The gas limit determines the maximum gas that can be spent in the transaction - * @param doStaticCall Flag to determinate whether to perform a callStatic to work or not. Defaults to true. - * - */ -export class FlashbotsBroadcastor { - public chainId: number; - - constructor( - public flashbotsProvider: FlashbotsBundleProvider, - public priorityFeeInWei: number, - public gasLimit: number, - public doStaticCall = true - ) { - this.chainId = flashbotsProvider.network.chainId; - } - - async tryToWorkOnFlashbots(props: BroadcastorProps): Promise { - const { jobContract, workMethod, workArguments, block } = props; - - if (this.doStaticCall) { - try { - await jobContract.callStatic[workMethod](...workArguments); - } catch (error: unknown) { - if (error instanceof Error) { - console.log(`Static call failed with ${error.message}`); - } - return; - } - } - - const { priorityFee, maxFeePerGas } = getGasParametersNextBlock({ block, priorityFeeInWei: this.priorityFeeInWei }); - - const txSigner = jobContract.signer as Wallet; - - const currentNonce = await txSigner.getTransactionCount(); - - const options: Overrides = { - gasLimit: this.gasLimit, - nonce: currentNonce, - maxFeePerGas, - maxPriorityFeePerGas: priorityFee, - type: 2, - }; - - const tx: TransactionRequest = await populateTx({ - contract: jobContract, - functionName: workMethod, - functionArgs: [...workArguments], - options, - chainId: this.chainId, - }); - - const privateTx: FlashbotsBundleTransaction = { - transaction: tx, - signer: txSigner, - }; - - console.log('Transaction populated successfully. Sending bundle...'); - - await sendAndHandleResponse({ flashbotsProvider: this.flashbotsProvider, privateTx }); - } -} diff --git a/src/broadcastors/index.ts b/src/broadcastors/index.ts index cc3f43b..936f9b2 100644 --- a/src/broadcastors/index.ts +++ b/src/broadcastors/index.ts @@ -1,4 +1,3 @@ -export * from './flashbotsBroadcastor'; +export * from './privateBroadcastor'; export * from './mempoolBroadcastor'; export * from './stealthBroadcastor'; -export * from './privateBroadcastor'; diff --git a/src/broadcastors/mempoolBroadcastor.ts b/src/broadcastors/mempoolBroadcastor.ts index 2093068..4959637 100644 --- a/src/broadcastors/mempoolBroadcastor.ts +++ b/src/broadcastors/mempoolBroadcastor.ts @@ -20,7 +20,7 @@ export class MempoolBroadcastor { public doStaticCall = true ) {} - tryToWorkOnMempool = async (props: BroadcastorProps): Promise => { + tryToWork = async (props: BroadcastorProps): Promise => { const { jobContract, workMethod, workArguments, block } = props; const txSigner = jobContract.signer as Wallet; diff --git a/src/broadcastors/privateBroadcastor.ts b/src/broadcastors/privateBroadcastor.ts index 1b219e6..b20c001 100644 --- a/src/broadcastors/privateBroadcastor.ts +++ b/src/broadcastors/privateBroadcastor.ts @@ -1,14 +1,12 @@ -import { getStealthHash } from '../flashbots'; -import { getGasParametersNextBlock, populateTx, sendPrivateTransaction } from '../transactions'; +import { getMainnetGasType2Parameters, populateTx, sendBundle } from '../transactions'; import type { TransactionRequest } from '@ethersproject/abstract-provider'; -import { Contract, Overrides, Wallet, ethers } from 'ethers'; +import { Wallet, Overrides, ethers } from 'ethers'; import { BroadcastorProps } from 'types'; /** * @notice Creates and populate a private transaction to work a specific job * - * @param endpoint The endpoint url - * @param stealthRelayer The address of the StealthRelayer contract. + * @param endpoints The endpoint urls * @param priorityFeeInWei The priority fee in wei * @param gasLimit The gas limit determines the maximum gas that can be spent in the transaction * @param doStaticCall Flag to determinate whether to perform a callStatic to work or not. Defaults to true. @@ -17,23 +15,19 @@ import { BroadcastorProps } from 'types'; */ export class PrivateBroadcastor { constructor( - public endpoint: string, - public stealthRelayer: Contract, + public endpoints: string[], public priorityFeeInWei: number, public gasLimit: number, public doStaticCall = true, public chainId: number ) {} - async tryToWorkOnStealthRelayer(props: BroadcastorProps): Promise { + async tryToWork(props: BroadcastorProps): Promise { const { jobContract, workMethod, workArguments, block } = props; - const stealthHash = getStealthHash(); - const workData = jobContract.interface.encodeFunctionData(workMethod, [...workArguments]); - if (this.doStaticCall) { try { - await this.stealthRelayer.callStatic.execute(jobContract.address, workData, stealthHash, block.number); + await jobContract.callStatic[workMethod](...workArguments); } catch (error: unknown) { if (error instanceof Error) { console.log(`Static call failed with ${error.message}`); @@ -42,9 +36,14 @@ export class PrivateBroadcastor { } } - console.log(`Attempting to work strategy statically succeeded. Preparing real transaction...`); + const blocksAhead = 2; + const targetBlock = block.number + blocksAhead; - const { priorityFee, maxFeePerGas } = getGasParametersNextBlock({ block, priorityFeeInWei: this.priorityFeeInWei }); + const { priorityFee, maxFeePerGas } = getMainnetGasType2Parameters({ + block, + priorityFeeInWei: this.priorityFeeInWei, + blocksAhead, + }); const txSigner = jobContract.signer as Wallet; @@ -57,22 +56,22 @@ export class PrivateBroadcastor { maxPriorityFeePerGas: priorityFee, type: 2, }; + const tx: TransactionRequest = await populateTx({ - contract: this.stealthRelayer, - functionName: 'execute', - functionArgs: [jobContract.address, workData, stealthHash, block.number], + contract: jobContract, + functionName: workMethod, + functionArgs: [...workArguments], options, chainId: this.chainId, }); const privateTx = await txSigner.signTransaction(tx); + console.log(`Bundle populated successfully. Sending private bundle: ${workArguments}`); - console.log(`Transaction populated successfully. Sending private transaction for strategy: ${workArguments}`); - - await sendPrivateTransaction({ - endpoint: this.endpoint, + await sendBundle({ + endpoints: this.endpoints, privateTx, - maxBlockNumber: ethers.utils.hexlify(block.number).toString(), + targetBlock: ethers.utils.hexlify(targetBlock).toString(), }); } } diff --git a/src/broadcastors/stealthBroadcastor.ts b/src/broadcastors/stealthBroadcastor.ts index 5940947..d5ac02c 100644 --- a/src/broadcastors/stealthBroadcastor.ts +++ b/src/broadcastors/stealthBroadcastor.ts @@ -1,36 +1,31 @@ -import { calculateTargetBlocks, getStealthHash } from '../flashbots'; +import { getStealthHash } from '../flashbots'; import { getMainnetGasType2Parameters, populateTx, sendBundle } from '../transactions'; import type { TransactionRequest } from '@ethersproject/abstract-provider'; -import type { FlashbotsBundleTransaction, FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle'; -import type { Wallet, Overrides, Contract } from 'ethers'; +import { Contract, Overrides, Wallet, ethers } from 'ethers'; import { BroadcastorProps } from 'types'; /** - * @notice Creates and populate a transaction for work in a determinated job using flashbots + * @notice Creates and populate a private transaction to work a specific job * - * @param flashbotsProvider The flashbots provider. It contains a JSON or WSS provider - * @param flashbots The flashbot that will send the bundle - * @param burstSize The amount of transactions for future blocks to be broadcast each time + * @param endpoints The endpoint urls + * @param stealthRelayer The address of the StealthRelayer contract. * @param priorityFeeInWei The priority fee in wei - * @param gasLimit The gas limit determines the maximum gas that can be spent in the transaction - * @param doStaticCall Flag to determinate whether to perform a callStatic to work or not. Defaults to true. + * @param gasLimit The gas limit determines the maximum gas that can be spent in the transaction + * @param doStaticCall Flag to determinate whether to perform a callStatic to work or not. Defaults to true. + * @param chainId The chainId. * */ export class StealthBroadcastor { - public chainId: number; - constructor( - public flashbotsProvider: FlashbotsBundleProvider, + public endpoints: string[], public stealthRelayer: Contract, public priorityFeeInWei: number, public gasLimit: number, - public burstSize: number, - public doStaticCall = true - ) { - this.chainId = flashbotsProvider.network.chainId; - } + public doStaticCall = true, + public chainId: number + ) {} - async tryToWorkOnStealthRelayer(props: BroadcastorProps): Promise { + async tryToWork(props: BroadcastorProps): Promise { const { jobContract, workMethod, workArguments, block } = props; const stealthHash = getStealthHash(); @@ -47,16 +42,15 @@ export class StealthBroadcastor { } } - console.log(`Attempting to work strategy statically succeeded. Preparing real transaction...`); + console.log(`Attempting to work strategy statically succeeded. Preparing real bundle...`); - const nextBlock = ++block.number; - - const targetBlocks = calculateTargetBlocks(this.burstSize, nextBlock); + const blocksAhead = 2; + const targetBlock = block.number + blocksAhead; const { priorityFee, maxFeePerGas } = getMainnetGasType2Parameters({ block, priorityFeeInWei: this.priorityFeeInWei, - burstSize: this.burstSize, + blocksAhead, }); const txSigner = jobContract.signer as Wallet; @@ -70,23 +64,23 @@ export class StealthBroadcastor { maxPriorityFeePerGas: priorityFee, type: 2, }; - for (const targetBlock of targetBlocks) { - const tx: TransactionRequest = await populateTx({ - contract: this.stealthRelayer, - functionName: 'execute', - functionArgs: [jobContract.address, workData, stealthHash, targetBlock], - options, - chainId: this.chainId, - }); - const privateTx: FlashbotsBundleTransaction = { - transaction: tx, - signer: txSigner, - }; + const tx: TransactionRequest = await populateTx({ + contract: this.stealthRelayer, + functionName: 'execute', + functionArgs: [jobContract.address, workData, stealthHash, targetBlock], + options, + chainId: this.chainId, + }); - console.log('Transaction populated successfully. Sending bundle...'); + const privateTx = await txSigner.signTransaction(tx); - await sendBundle({ flashbotsProvider: this.flashbotsProvider, privateTxs: [privateTx], targetBlockNumber: targetBlock }); - } + console.log(`Bundle populated successfully. Sending private bundle for strategy: ${workArguments}`); + + await sendBundle({ + endpoints: this.endpoints, + privateTx, + targetBlock: ethers.utils.hexlify(targetBlock).toString(), + }); } } diff --git a/src/subscriptions/blocks.ts b/src/subscriptions/blocks.ts index ff9972a..2df2300 100644 --- a/src/subscriptions/blocks.ts +++ b/src/subscriptions/blocks.ts @@ -9,6 +9,9 @@ type CallbackFunction = (block: Block) => Promise; * */ export class BlockListener { + private destroyed = false; + private intervals: NodeJS.Timeout[] = []; + /** * @param provider - JsonRpc provider that has the methods needed to fetch and listen for new blocks. */ @@ -32,6 +35,8 @@ export class BlockListener { */ stream(cb: CallbackFunction, intervalDelay = 0, blockDelay = 0): void { const start = async () => { + if (this.destroyed) return; + // save latest block number, in order to avoid old block dumps let latestBlockNumber = await this.provider.getBlockNumber(); @@ -39,6 +44,8 @@ export class BlockListener { // listen for next block this.provider.on('block', async (blockNumber) => { + if (this.destroyed) return; + // avoid having old dump of blocks if (blockNumber <= latestBlockNumber) return; latestBlockNumber = blockNumber; @@ -50,6 +57,8 @@ export class BlockListener { // delay the block arrival a bit, for ankr to have time to sync setTimeout(async () => { + if (this.destroyed) return; + // double check that the block to process is actually the latest if (blockNumber < latestBlockNumber) return; @@ -67,11 +76,19 @@ export class BlockListener { if (intervalDelay > 0) { // get next block every {intervalDelay} of sleep - setInterval(start, intervalDelay); + const interval = setInterval(start, intervalDelay); + this.intervals.push(interval); } } stop(): void { this.provider.removeAllListeners('block'); } + + destroy(): void { + this.destroyed = true; + this.stop(); + this.intervals.forEach((interval) => clearInterval(interval)); + this.intervals = []; + } } diff --git a/src/transactions/getMainnetGasType2Parameters.ts b/src/transactions/getMainnetGasType2Parameters.ts index 93807d3..1f9f1eb 100644 --- a/src/transactions/getMainnetGasType2Parameters.ts +++ b/src/transactions/getMainnetGasType2Parameters.ts @@ -14,17 +14,17 @@ import { BigNumber } from 'ethers'; * @return An object containing the provided priority fee in gwei and the calculated maxFeePerGas. */ export function getMainnetGasType2Parameters(props: GetMainnetGasType2ParametersProps): GasType2Parameters { - const { block, priorityFeeInWei, burstSize } = props; + const { block, priorityFeeInWei, blocksAhead } = props; if (!block.baseFeePerGas) { throw new Error('Missing property baseFeePerGas on block'); } - if (burstSize === 0 || burstSize === 1) { + if (blocksAhead === 0 || blocksAhead === 1) { return getGasParametersNextBlock({ block, priorityFeeInWei }); } - const maxBaseFee = FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(block.baseFeePerGas, burstSize); + const maxBaseFee = FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(block.baseFeePerGas, blocksAhead); const priorityFee = BigNumber.from(priorityFeeInWei); const maxFeePerGas = maxBaseFee.add(priorityFee); diff --git a/src/transactions/sendAndHandleResponse.ts b/src/transactions/sendAndHandleResponse.ts index cd81c35..6998a5c 100644 --- a/src/transactions/sendAndHandleResponse.ts +++ b/src/transactions/sendAndHandleResponse.ts @@ -1,6 +1,6 @@ import { SendAndHandleResponseProps } from 'types'; -export async function sendAndHandleResponse(props: SendAndHandleResponseProps) { +export async function sendAndHandleResponse(props: SendAndHandleResponseProps): Promise { const { flashbotsProvider, privateTx, maxBlockNumber } = props; try { diff --git a/src/transactions/sendBundle.ts b/src/transactions/sendBundle.ts index b5115fd..dcdc11e 100644 --- a/src/transactions/sendBundle.ts +++ b/src/transactions/sendBundle.ts @@ -1,39 +1,40 @@ -import { SendBundleProps } from 'types'; - -export async function sendBundle(props: SendBundleProps) { - const { flashbotsProvider, privateTxs, targetBlockNumber } = props; - - try { - const response = await flashbotsProvider.sendBundle(privateTxs, targetBlockNumber); - - if ('error' in response) { - console.warn(`Transaction execution error`, response.error); - return; - } - - const simulation = await response.simulate(); - if ('error' in simulation || simulation.firstRevert) { - console.error(`Transaction simulation error`, simulation); - return; - } - - console.debug(`Transaction simulation success`, simulation); - - const resolution = await response.wait(); - console.log(resolution); - - if (resolution === 0) { - console.log(`=================== TX INCLUDED =======================`); - } else if (resolution === 1) { - console.log(`==================== TX DROPPED =======================`); - } - } catch (error: unknown) { - if (error === 'Timed out') { - console.debug( - 'One of the sent Transactions timed out. This means around 20 blocks have passed and Flashbots has ceased retrying it.' - ); +import axios from 'axios'; +import { SendPrivateBundleProps } from 'types'; + +export async function sendBundle(props: SendPrivateBundleProps): Promise { + const { endpoints, privateTx, targetBlock } = props; + + const requestData = { + jsonrpc: '2.0', + method: 'eth_sendBundle', + params: [ + { + txs: [privateTx], + blockNumber: targetBlock, + }, + ], + id: '1', + }; + + const promises = endpoints.map(async (endpoint) => { + try { + const response = await axios.post(endpoint, requestData, { + headers: { + 'Content-Type': 'application/json', + }, + }); + console.log(response.data); + } catch (error) { + if (axios.isAxiosError(error)) { + console.error('Axios error message:', error.message); + if (error.response) { + console.log('Response data:', error.response.data); + } + } else { + console.error('Unexpected error:', error); + } } + }); - console.log(error); - } + await Promise.all(promises); } diff --git a/src/transactions/sendPrivateTransaction.ts b/src/transactions/sendPrivateTransaction.ts index 726dc21..ee8c294 100644 --- a/src/transactions/sendPrivateTransaction.ts +++ b/src/transactions/sendPrivateTransaction.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { SendPrivateTransactionProps } from 'types'; -export async function sendPrivateTransaction(props: SendPrivateTransactionProps) { +export async function sendPrivateTransaction(props: SendPrivateTransactionProps): Promise { const { endpoint, privateTx, maxBlockNumber } = props; const requestData = { @@ -24,7 +24,6 @@ export async function sendPrivateTransaction(props: SendPrivateTransactionProps) }); console.log(response.data); - return; } catch (error) { if (axios.isAxiosError(error)) { console.error('Axios error message:', error.message); @@ -34,6 +33,5 @@ export async function sendPrivateTransaction(props: SendPrivateTransactionProps) } else { console.error('Unexpected error:', error); } - return; } } diff --git a/src/types/Transactions.ts b/src/types/Transactions.ts index b2b38ad..4067adc 100644 --- a/src/types/Transactions.ts +++ b/src/types/Transactions.ts @@ -36,12 +36,12 @@ export interface SendTxProps { * * @param block The current block. * @param priorityFeeInWei The priority fee that will be used for the transaction being formatted. - * @param burstSize The number blocks to send the transaction to. Can also be interpreted as the number of blocks into the future to use when calculating the maximum base fee. + * @param blocksAhead The number blocks to send the transaction to. Can also be interpreted as the number of blocks into the future to use when calculating the maximum base fee. */ export interface GetMainnetGasType2ParametersProps { block: Block; priorityFeeInWei: number; - burstSize: number; + blocksAhead: number; } /** @@ -94,3 +94,16 @@ export interface GetGasParametersNextBlockProps { block: Block; priorityFeeInWei: number; } + +/** + * @notice sendPrivateBundle includes all parameters required to call sendPrivateBundle function + * + * @param endpoints The endpoints to hit. + * @param privateTx The private flashbots transaction to send serialized. + * @param targetBlock The block number where this bundle will be valid. + */ +export interface SendPrivateBundleProps { + endpoints: string[]; + privateTx: string; + targetBlock?: string; +}