Skip to content
Open
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
121 changes: 121 additions & 0 deletions src/demo_broadcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { BigNumber, providers, Wallet } from 'ethers'
import { FlashbotsBundleProvider, FlashbotsBundleResolution, BuilderBroadcaster } from './index'
import { TransactionRequest } from '@ethersproject/abstract-provider'
import { v4 as uuidv4 } from 'uuid'

const FLASHBOTS_AUTH_KEY = process.env.FLASHBOTS_AUTH_KEY

const GWEI = BigNumber.from(10).pow(9)
const PRIORITY_FEE = GWEI.mul(3)
const LEGACY_GAS_PRICE = GWEI.mul(57)
const BLOCKS_IN_THE_FUTURE = 2

// ===== Uncomment this for mainnet =======
const CHAIN_ID = 1
const provider = new providers.JsonRpcProvider(
{ url: process.env.ETHEREUM_RPC_URL || 'http://127.0.0.1:8545' },
{ chainId: CHAIN_ID, ensAddress: '', name: 'mainnet' }
)
const FLASHBOTS_EP = 'https://relay.flashbots.net/'
// ===== Uncomment this for mainnet =======

// ===== Uncomment this for Goerli =======
// const CHAIN_ID = 5
// const provider = new providers.InfuraProvider(CHAIN_ID, process.env.INFURA_API_KEY)
// const FLASHBOTS_EP = 'https://relay-goerli.flashbots.net/'
// ===== Uncomment this for Goerli =======

for (const e of ['FLASHBOTS_AUTH_KEY', 'INFURA_API_KEY', 'ETHEREUM_RPC_URL', 'PRIVATE_KEY']) {
if (!process.env[e]) {
// don't warn for skipping ETHEREUM_RPC_URL if using goerli
if (FLASHBOTS_EP.includes('goerli') && e === 'ETHEREUM_RPC_URL') {
continue
}
console.warn(`${e} should be defined as an environment variable`)
}
}

async function main() {
const authSigner = FLASHBOTS_AUTH_KEY ? new Wallet(FLASHBOTS_AUTH_KEY) : Wallet.createRandom()
const wallet = new Wallet(process.env.PRIVATE_KEY || '', provider)
const flashbotsProvider = await BuilderBroadcaster.createBroadcaster(
provider,
authSigner,
[
"https://relay.flashbots.net",
"https://rpc.titanbuilder.xyz",
"https://builder0x69.io",
"https://rpc.beaverbuild.org",
"https://rsync-builder.xyz",
"https://api.blocknative.com/v1/auction",
// "https://mev.api.blxrbdn.com", # Authentication required
"https://eth-builder.com",
"https://builder.gmbit.co/rpc",
"https://buildai.net",
"https://rpc.payload.de",
"https://rpc.lightspeedbuilder.info",
"https://rpc.nfactorial.xyz",
]
)

const legacyTransaction = {
to: wallet.address,
gasPrice: LEGACY_GAS_PRICE,
gasLimit: 21000,
data: '0x',
nonce: await provider.getTransactionCount(wallet.address),
chainId: CHAIN_ID
}

provider.on('block', async (blockNumber) => {
const block = await provider.getBlock(blockNumber)
const replacementUuid = uuidv4()

let eip1559Transaction: TransactionRequest
if (block.baseFeePerGas == null) {
console.warn('This chain is not EIP-1559 enabled, defaulting to two legacy transactions for demo')
eip1559Transaction = { ...legacyTransaction }
// We set a nonce in legacyTransaction above to limit validity to a single landed bundle. Delete that nonce for tx#2, and allow bundle provider to calculate it
delete eip1559Transaction.nonce
} else {
const maxBaseFeeInFutureBlock = FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(block.baseFeePerGas, BLOCKS_IN_THE_FUTURE)
eip1559Transaction = {
to: wallet.address,
type: 2,
maxFeePerGas: PRIORITY_FEE.add(maxBaseFeeInFutureBlock),
maxPriorityFeePerGas: PRIORITY_FEE,
gasLimit: 21000,
data: '0x',
chainId: CHAIN_ID
}
}

const signedTransactions = await flashbotsProvider.signBundle([
{
signer: wallet,
transaction: legacyTransaction
},
{
signer: wallet,
transaction: eip1559Transaction
}
])
const targetBlock = blockNumber + BLOCKS_IN_THE_FUTURE

const bundleSubmission = await flashbotsProvider.broadcastBundle(signedTransactions, targetBlock, { replacementUuid })
console.log('bundle submitted, waiting')
if ('error' in bundleSubmission) {
throw new Error(bundleSubmission.error.message)
}

const waitResponse = await bundleSubmission.wait()
console.log(`Wait Response: ${FlashbotsBundleResolution[waitResponse]}`)
if (waitResponse === FlashbotsBundleResolution.BundleIncluded || waitResponse === FlashbotsBundleResolution.AccountNonceTooHigh) {
process.exit(0)
} else {
console.log(bundleSubmission.bundleHashes)
}
})
}

main()
132 changes: 128 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ export interface FlashbotsPrivateTransactionResponse {
receipts: () => Promise<Array<TransactionReceipt>>
}

export interface BundleBroadcastResponse {
bundleTransactions: Array<TransactionAccountNonce>
wait: () => Promise<FlashbotsBundleResolution>
receipts: () => Promise<Array<TransactionReceipt>>
bundleHashes: Array<string>
}

export interface TransactionSimulationBase {
txHash: string
gasUsed: number
Expand Down Expand Up @@ -113,6 +120,8 @@ export type SimulationResponse = SimulationResponseSuccess | RelayResponseError

export type FlashbotsTransaction = FlashbotsTransactionResponse | RelayResponseError

export type BundleBroadcast = BundleBroadcastResponse | RelayResponseError

export type FlashbotsPrivateTransaction = FlashbotsPrivateTransactionResponse | RelayResponseError

export interface GetUserStatsResponseSuccess {
Expand Down Expand Up @@ -233,7 +242,7 @@ const TIMEOUT_MS = 5 * 60 * 1000

export class FlashbotsBundleProvider extends providers.JsonRpcProvider {
private genericProvider: BaseProvider
private authSigner: Signer
protected authSigner: Signer
private connectionInfo: ConnectionInfo

constructor(genericProvider: BaseProvider, authSigner: Signer, connectionInfoOrUrl: ConnectionInfo, network: Networkish) {
Expand Down Expand Up @@ -602,7 +611,7 @@ export class FlashbotsBundleProvider extends providers.JsonRpcProvider {
* @param targetBlockNumber block number to check for bundle inclusion
* @param timeout ms
*/
private waitForBundleInclusion(transactionAccountNonces: Array<TransactionAccountNonce>, targetBlockNumber: number, timeout: number) {
protected waitForBundleInclusion(transactionAccountNonces: Array<TransactionAccountNonce>, targetBlockNumber: number, timeout: number) {
return new Promise<FlashbotsBundleResolution>((resolve, reject) => {
let timer: NodeJS.Timer | null = null
let done = false
Expand Down Expand Up @@ -1086,11 +1095,11 @@ export class FlashbotsBundleProvider extends providers.JsonRpcProvider {
return fetchJson(connectionInfo, request)
}

private async fetchReceipts(bundledTransactions: Array<TransactionAccountNonce>): Promise<Array<TransactionReceipt>> {
protected async fetchReceipts(bundledTransactions: Array<TransactionAccountNonce>): Promise<Array<TransactionReceipt>> {
return Promise.all(bundledTransactions.map((bundledTransaction) => this.genericProvider.getTransactionReceipt(bundledTransaction.hash)))
}

private prepareRelayRequest(
protected prepareRelayRequest(
method:
| 'eth_callBundle'
| 'eth_cancelBundle'
Expand All @@ -1111,3 +1120,118 @@ export class FlashbotsBundleProvider extends providers.JsonRpcProvider {
}
}
}

export class BuilderBroadcaster extends FlashbotsBundleProvider {
private connectionInfoArr: Array<ConnectionInfo>;

constructor(genericProvider: BaseProvider, authSigner: Signer , network: Networkish, connectionInfoOrUrls: Array<ConnectionInfo>) {
const defaultConnectionInfo: ConnectionInfo = { url: DEFAULT_FLASHBOTS_RELAY }
super(genericProvider, authSigner, defaultConnectionInfo, network)
this.connectionInfoArr = connectionInfoOrUrls
}

/**
* Creates a new Builder Bundle Broadcaster.
* @param genericProvider ethers.js mainnet provider
* @param authSigner account to sign bundles
* @param connectionInfoOrUrl (optional) connection settings
* @param network (optional) network settings
*
* @example
* ```typescript
* const {providers, Wallet} = require("ethers")
* const {BuilderBroadcaster} = require("@flashbots/ethers-provider-bundle")
* const authSigner = Wallet.createRandom()
* const provider = new providers.JsonRpcProvider("http://localhost:8545")
* const broadcaster = await BuilderBroadcaster.create(provider, authSigner, ['https://relay.flashbots.net/'])
* ```
*/
static async createBroadcaster(
genericProvider: BaseProvider,
authSigner: Signer,
builderEndpoints: Array<string>,
network?: Networkish
): Promise<BuilderBroadcaster> {
const connectionInfoOrUrlArray: ConnectionInfo[] = Array.isArray(builderEndpoints)
? builderEndpoints.map((b_e) => ({ url: b_e }))
: [];

const networkish: Networkish = {
chainId: 0,
name: ''
}
if (typeof network === 'string') {
networkish.name = network
} else if (typeof network === 'number') {
networkish.chainId = network
} else if (typeof network === 'object') {
networkish.name = network.name
networkish.chainId = network.chainId
}

if (networkish.chainId === 0) {
networkish.chainId = (await genericProvider.getNetwork()).chainId
}

return new BuilderBroadcaster(genericProvider, authSigner, networkish, connectionInfoOrUrlArray)
}

public async broadcastBundle(
signedBundledTransactions: Array<string>,
targetBlockNumber: number,
opts?: FlashbotsOptions
): Promise<BundleBroadcast> {
const params = {
txs: signedBundledTransactions,
blockNumber: `0x${targetBlockNumber.toString(16)}`,
minTimestamp: opts?.minTimestamp,
maxTimestamp: opts?.maxTimestamp,
revertingTxHashes: opts?.revertingTxHashes,
replacementUuid: opts?.replacementUuid
}

const request = JSON.stringify(super.prepareRelayRequest('eth_sendBundle', [params]))
const responses = await this.requestBroadcast(request)

const bundleTransactions = signedBundledTransactions.map((signedTransaction) => {
const transactionDetails = ethers.utils.parseTransaction(signedTransaction)
return {
signedTransaction,
hash: ethers.utils.keccak256(signedTransaction),
account: transactionDetails.from || '0x0',
nonce: transactionDetails.nonce
}
})

const bundleHashes = responses
.filter((response) => response.error === undefined || response.error === null)
.map((response) => response.result?.bundleHash)
.filter(Boolean);

return {
bundleTransactions,
wait: () => super.waitForBundleInclusion(bundleTransactions, targetBlockNumber, TIMEOUT_MS),
receipts: () => super.fetchReceipts(bundleTransactions),
bundleHashes: bundleHashes
}

}

private async requestBroadcast(request: string) {
const responseHandles = new Array(); [];
for (let connectionInfo of this.connectionInfoArr) {
const updatedConnectionInfo = { ...connectionInfo };
updatedConnectionInfo.headers = {
'X-Flashbots-Signature': `${await this.authSigner.getAddress()}:${await this.authSigner.signMessage(id(request))}`,
...connectionInfo.headers
};
const promise = new Promise((resolve) => resolve(fetchJson(updatedConnectionInfo, request)));
responseHandles.push(promise);
}

let responses = await Promise.all(responseHandles)
return responses
}


}