Skip to content
43 changes: 43 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"ethers": "^5.4.2"
},
"dependencies": {
"@ethersproject/rlp": "^5.4.0",
"eciesjs": "^0.3.11",
"ts-node": "^9.1.0",
"typescript": "^4.1.2"
}
Expand Down
123 changes: 120 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { BlockTag, TransactionReceipt, TransactionRequest } from '@ethersproject/abstract-provider'
import { Networkish } from '@ethersproject/networks'
import { BaseProvider } from '@ethersproject/providers'
import { BaseProvider, TransactionResponse } from '@ethersproject/providers'
import { ConnectionInfo, fetchJson } from '@ethersproject/web'
import { BigNumber, ethers, providers, Signer } from 'ethers'
import { id } from 'ethers/lib/utils'
import { encode } from '@ethersproject/rlp'
import { encrypt } from 'eciesjs'

export const DEFAULT_FLASHBOTS_RELAY = 'https://relay.flashbots.net'

Expand All @@ -28,6 +30,12 @@ export interface FlashbotsOptions {
revertingTxHashes?: Array<string>
}

export interface FlashbotsBundle {
signedBundledTransactions: Array<string>
blockTarget: number
options?: FlashbotsOptions
}

export interface TransactionAccountNonce {
hash: string
signedTransaction: string
Expand Down Expand Up @@ -352,7 +360,7 @@ export class FlashbotsBundleProvider extends providers.JsonRpcProvider {
public async getBundleStats(bundleHash: string, blockNumber: number): Promise<GetBundleStatsResponse> {
const evmBlockNumber = `0x${blockNumber.toString(16)}`

const params = [{bundleHash, blockNumber: evmBlockNumber}]
const params = [{ bundleHash, blockNumber: evmBlockNumber }]
const request = JSON.stringify(this.prepareBundleRequest('flashbots_getBundleStats', params))
const response = await this.request(request)
if (response.error !== undefined && response.error !== null) {
Expand Down Expand Up @@ -415,6 +423,112 @@ export class FlashbotsBundleProvider extends providers.JsonRpcProvider {
}
}

/**
* Method to send a carrier tx into the public mempool
*
* @param bundle FlashbotsBundle with AT LEAST signed bundled transactions in signedBundledTransactions field obtained
* from {@link signBundle} method, and blockTarget.
* @param validatorPublicKey The public key of the validator that will be able to decrypt the bundle and include it
* into the bundle pool.
* @param signer Signer who will sign the carrier transaction.
* @param carrierTx TransactionRequest whose data field will carry the encrypted bundle : MAY be an incomplete
* object which will be populated with default values.
*
* @return Promise<TransactionResponse> Promise containing the response for the carrier tx
* */

public async sendCarrierTransaction(
bundle: FlashbotsBundle,
validatorPublicKey: string,
signer: Signer,
carrierTx: TransactionRequest
): Promise<TransactionResponse> {
//RLP-serialize the given bundle
const serializedBundle = this.rlpSerializeBundle(bundle)

//Encrypt the encoded bundle with the passed validator pub_key
const encryptedBundle = encrypt(validatorPublicKey, Buffer.from(serializedBundle))

//Populate carrier_tx.data as : carrier_tx.data = MEV_Prefix | validator pub_key | Encrypt(validator pub_key, serialized bundle)
const mevPrefix = `0123` //this is a placeholder!

let payload = `0x`
payload += mevPrefix
payload += validatorPublicKey
payload += encryptedBundle.toString('hex')

carrierTx.data = payload

//Check if carrier_tx has minimum params, populate with defaults if not
/*
The following statement is intended to be used in order to support any type of incomplete TransactionRequest
received, populating it with default values if any one is missing
*/
await this.populateCarrierTransaction(carrierTx, signer)

//Sign the transaction received as param with passed signer
const signedTx = await signer.signTransaction(carrierTx)

//Propagate carrier_tx into the public mempool and return Promise<TransactionResponse> for the carrier_tx
return this.genericProvider.sendTransaction(signedTx)
}

/**
* A private method to encode a FlashbotsBundle following the RLP serialization standard
* @param bundle the FlashbotsBundle instance to be serialized
* @return string the rlp encoded bundle
* @private
*/
private rlpSerializeBundle(bundle: FlashbotsBundle): string {
if (bundle.signedBundledTransactions === undefined || bundle.signedBundledTransactions.length === 0)
throw Error('Bundle has no transactions')
if (bundle.options === undefined) bundle.options = {}

const fields = [
bundle.signedBundledTransactions,
this.formatNumber(bundle.blockTarget || 0),
this.formatNumber(bundle.options.minTimestamp || 0),
this.formatNumber(bundle.options.maxTimestamp || 0),
bundle.options.revertingTxHashes || []
]
return encode(fields)
}

private formatNumber(num: number): string {
const hexNum = num.toString(16)
return hexNum.length % 2 === 0 ? `0x${hexNum}` : `0x0${hexNum}`
}

/**
* A private method to populate {@param carrier}'s missing fields with default values
* @param carrier an instance of TransactionRequest which will be the tx containing the full payload in its data field
* @param signer the signer Object which will send the carrier tx
* @private
*/
private async populateCarrierTransaction(carrier: TransactionRequest, signer: Signer) {
if (!('to' in carrier)) throw Error('carrier.to field is missing')

if (carrier.gasPrice != null) {
const gasPrice = BigNumber.from(carrier.gasPrice)
const maxFeePerGas = BigNumber.from(carrier.maxFeePerGas || 0)
if (!gasPrice.eq(maxFeePerGas)) {
throw Error('carrier tx EIP-1559 mismatch: gasPrice != maxFeePerGas')
}
}
const latestBlock = await this.genericProvider.getBlock('latest')
const blocksInFuture = 5
const maxBaseFeeInFuture = FlashbotsBundleProvider.getMaxBaseFeeInFutureBlock(<BigNumber>latestBlock.baseFeePerGas, blocksInFuture)

carrier.type = 2
carrier.chainId = carrier.chainId || 1
carrier.nonce = carrier.nonce || (await this.genericProvider.getTransactionCount(signer.getAddress()))
carrier.maxPriorityFeePerGas = carrier.maxPriorityFeePerGas || ethers.utils.parseUnits('1.5', 'gwei')
carrier.maxFeePerGas = carrier.maxFeePerGas || maxBaseFeeInFuture.add(carrier.maxPriorityFeePerGas)
carrier.gasLimit = carrier.gasLimit || (await this.genericProvider.estimateGas(carrier))
carrier.value = carrier.value || 0
carrier.accessList = carrier.accessList || []
}

private async request(request: string) {
const connectionInfo = { ...this.connectionInfo }
connectionInfo.headers = {
Expand All @@ -428,7 +542,10 @@ export class FlashbotsBundleProvider extends providers.JsonRpcProvider {
return Promise.all(bundledTransactions.map((bundledTransaction) => this.genericProvider.getTransactionReceipt(bundledTransaction.hash)))
}

private prepareBundleRequest(method: 'eth_callBundle' | 'eth_sendBundle' | 'flashbots_getUserStats' | 'flashbots_getBundleStats', params: RpcParams) {
private prepareBundleRequest(
method: 'eth_callBundle' | 'eth_sendBundle' | 'flashbots_getUserStats' | 'flashbots_getBundleStats',
params: RpcParams
) {
return {
method: method,
params: params,
Expand Down