flashbots-builder
Version:
"Flashbots protocol enables a crypto wallet to submit a sequence of ethereum transactions in one block, away from the prying eyes of frontrunning bots and miners, while being able to pay the gas fee for such transactions with another crypto wallet."
274 lines • 13.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const ethers_1 = require("ethers");
const ethers_provider_bundle_1 = require("@flashbots/ethers-provider-bundle");
const utils_1 = require("./utils");
const BLOCKS_IN_FUTURE = 2;
const GWEI = ethers_1.BigNumber.from(10).pow(9);
const PRIORITY_GAS_PRICE = GWEI.mul(31);
class FlashBotBuilder {
constructor() { }
addBundleProviderConnectionInfoOrUrl(bundleProviderConnectionInfoOrUrl) {
this.bundleProviderConnectionInfoOrUrl = bundleProviderConnectionInfoOrUrl;
return this;
}
addBundleProviderNetwork(bundleProviderNetwork) {
this.bundleProviderNetwork = bundleProviderNetwork;
return this;
}
addBlockchainNetworkId(blockchainNetworkId) {
this.blockchainNetworkId = blockchainNetworkId;
return this;
}
addBlockChainRpcProvider(blockChainRpcProvider) {
this.blockChainRpcProvider = blockChainRpcProvider;
return this;
}
addSponsorPrivateKey(sponsorPrivateKey) {
this.sponsorPrivateKey = sponsorPrivateKey;
return this;
}
addExecutorPrivateKey(executorPrivateKey) {
this.executorPrivateKey = executorPrivateKey;
return this;
}
addIntervalToFutureBlock(intervalToFutureBlock) {
this.intervalToFutureBlock = intervalToFutureBlock;
return this;
}
addPriorityGasPrice(priorityGasPrice) {
this.priorityGasPrice = priorityGasPrice;
return this;
}
static nullOrEmpty(value) {
return value === undefined || value === null || value.length === 0;
}
build() {
return new Promise((resolve, reject) => {
var errors = {};
if (!this.blockchainNetworkId || isNaN(this.blockchainNetworkId)) {
errors.blockchainNetworkId = "Not provided";
}
if (!this.blockChainRpcProvider) {
errors.blockChainRpcProvider = "Not provided";
}
if (FlashBotBuilder.nullOrEmpty(this.sponsorPrivateKey)) {
errors.sponsorPrivateKey = "Not provided";
}
if (FlashBotBuilder.nullOrEmpty(this.executorPrivateKey)) {
errors.executorPrivateKey = "Not provided";
}
if (Object.keys(errors).length > 0) {
reject(new Error(JSON.stringify(errors)));
}
else {
resolve(new FlashBot(this.blockchainNetworkId, this.blockChainRpcProvider, this.sponsorPrivateKey, this.executorPrivateKey, this.intervalToFutureBlock, this.priorityGasPrice, this.bundleProviderConnectionInfoOrUrl, this.bundleProviderNetwork));
}
});
}
}
class FlashBot {
constructor(blockchainNetworkId, blockChainRpcProvider, sponsorPrivateKey, executorPrivateKey, intervalToFutureBlock, priorityGasPrice, bundleProviderConnectionInfoOrUrl, bundleProviderNetwork) {
this.executorTransactions = [];
this.intervalToFutureBlock = BLOCKS_IN_FUTURE;
this.priorityGasPrice = PRIORITY_GAS_PRICE;
this.executionStopped = false;
this.blockchainNetworkId = blockchainNetworkId;
this.blockChainRpcProvider = blockChainRpcProvider;
this.sponsorWallet = (new ethers_1.Wallet(sponsorPrivateKey)).connect(blockChainRpcProvider);
this.executorWallet = (new ethers_1.Wallet(executorPrivateKey)).connect(blockChainRpcProvider);
if (intervalToFutureBlock)
this.intervalToFutureBlock = intervalToFutureBlock;
if (priorityGasPrice)
this.priorityGasPrice = priorityGasPrice;
this.bundleProviderConnectionInfoOrUrl = bundleProviderConnectionInfoOrUrl;
this.bundleProviderNetwork = bundleProviderNetwork;
}
static Builder() {
return new FlashBotBuilder();
}
getSponsorWallet() {
return this.sponsorWallet;
}
getExecutorWallet() {
return this.executorWallet;
}
getProvider() {
return this.blockChainRpcProvider;
}
addBundleTx(tx) {
this.executorTransactions.push(tx);
return this;
}
addMultipleBundleTx(txList) {
this.executorTransactions = this.executorTransactions.concat(txList);
return this;
}
exit() {
this.executionStopped = true;
this.getProvider().removeAllListeners("block");
}
execute(authSignerKey) {
return new Promise(async (resolve, reject) => {
if (this.executorTransactions.length == 0) {
reject(new Error("Empty transactions bundle"));
return;
}
let walletRelay;
var authSignerGenerated = false;
if (!authSignerKey) {
walletRelay = ethers_1.Wallet.createRandom();
authSignerKey = walletRelay.privateKey;
authSignerGenerated = true;
}
else {
walletRelay = new ethers_1.Wallet(authSignerKey);
}
const flashbotsProvider = await ethers_provider_bundle_1.FlashbotsBundleProvider.create(this.getProvider(), walletRelay, this.bundleProviderConnectionInfoOrUrl, this.bundleProviderNetwork);
//get the latest block
const block = await this.getProvider().getBlock("latest");
//the gas price
const gasPrice = this.priorityGasPrice.add(block.baseFeePerGas || 0);
//get total estimate
const gasEstimates = await Promise.all(this.executorTransactions.map(tx => this.getProvider().estimateGas({
...tx,
from: tx.from === undefined ? this.executorWallet.address : tx.from
})));
//sum up the total gas needed for the exeutor transactions
const gasEstimateTotal = gasEstimates.reduce((acc, cur) => acc.add(cur), ethers_1.BigNumber.from(0));
//Populate the transaction list
const bundleTransactions = [
{
transaction: {
chainId: this.blockchainNetworkId,
to: this.executorWallet.address,
gasPrice: gasPrice,
value: gasEstimateTotal.mul(gasPrice),
gasLimit: 21000,
},
signer: this.sponsorWallet
},
...this.executorTransactions.map((transaction, txNumber) => {
return {
transaction: {
chainId: this.blockchainNetworkId,
gasPrice: gasPrice,
gasLimit: gasEstimates[txNumber],
...transaction,
},
signer: this.executorWallet,
};
})
];
console.log("FlashbotsBundleTransaction[]:", bundleTransactions);
const signedBundle = await flashbotsProvider.signBundle(bundleTransactions);
await (0, utils_1.printTransactions)(bundleTransactions, signedBundle);
var simulatedGasPrice = await (0, utils_1.checkSimulation)(flashbotsProvider, signedBundle);
console.log(`Executor Account: ${this.executorWallet.address}`);
console.log(`Sponsor Account: ${this.sponsorWallet.address}`);
console.log(`Simulated Gas Price: ${(0, utils_1.gasPriceToGwei)(simulatedGasPrice)} gwei`);
console.log(`Gas Price: ${(0, utils_1.gasPriceToGwei)(gasPrice)} gwei`);
console.log(`Gas Used: ${gasEstimateTotal.toString()}`);
console.log(`Auth Signer Account${authSignerGenerated ? "(Generated!)" : ""}: ${walletRelay.address}`);
if (authSignerGenerated) {
console.log(`Your auth signer account was generated by the program.
\nIt's adviced to reuse it next time to build up reputation on the flasbots network.
\nThe generated auth signer private key will be returned with the response of this transactions bundle.
\nTransactions bundle Response format:
\n{
blockNumber: EXAMPLE_NUMBER_OF_BLOCK_TX_BUNDLE_WAS_ADDED_TO,
authSignerKey: EXAMPLE_GENERATED_OR_SUPPLIED_AUTH_SIGNER_KEY,
totalInclusionFails: TOTAL_NUMBER_OF_FAILED_TARGET_BLOCKS
}
\n`);
}
var lastBlock = 0;
var totalInclusionFails = 0;
var targetBlockNumber = block.number;
this.getProvider().on("block", async (blockNumber) => {
if (blockNumber <= lastBlock)
return;
lastBlock = blockNumber;
if (this.executionStopped) {
return;
}
try {
simulatedGasPrice = await (0, utils_1.checkSimulation)(flashbotsProvider, signedBundle);
}
catch (e) {
//console.log("TheErrorMessage", e.message, e.message.includes("nonce too low"))
if (e.message.includes("nonce too low")) {
this.exit();
resolve({
blockNumber: targetBlockNumber,
authSignerKey: authSignerKey,
totalInclusionFails: totalInclusionFails
});
}
else {
this.exit();
reject(new Error(JSON.stringify({
blockNumber: targetBlockNumber,
message: e.message,
totalInclusionFails: totalInclusionFails
})));
}
return;
}
targetBlockNumber = blockNumber + this.intervalToFutureBlock;
console.log(`Current Block Number: ${blockNumber}, Target Block Number:${targetBlockNumber}, gasPrice: ${(0, utils_1.gasPriceToGwei)(simulatedGasPrice)} gwei`);
if (this.executionStopped) {
return;
}
try {
const bundleResponse = await flashbotsProvider.sendBundle(bundleTransactions, targetBlockNumber);
if ("error" in bundleResponse) {
console.log("error:", bundleResponse.error.message);
return;
}
const bundleResolution = await bundleResponse.wait();
//console.log("bundleResolution", bundleResolution)
if (bundleResolution === ethers_provider_bundle_1.FlashbotsBundleResolution.BundleIncluded) {
this.exit();
resolve({
blockNumber: targetBlockNumber,
authSignerKey: authSignerKey,
totalInclusionFails: totalInclusionFails
});
}
else if (bundleResolution === ethers_provider_bundle_1.FlashbotsBundleResolution.BlockPassedWithoutInclusion) {
totalInclusionFails++;
console.log(`Not included in ${targetBlockNumber} | Total Inclusion Fails: ${totalInclusionFails}`);
}
else if (bundleResolution === ethers_provider_bundle_1.FlashbotsBundleResolution.AccountNonceTooHigh) {
/*This error sometimes occur when a transaction has already being submitted
So waiting/doing nothing will be better. The checkSimulation of the next block
listener will make a decision
if(!this.executionStopped) {
this.exit()
reject(new Error(
JSON.stringify({
blockNumber: targetBlockNumber,
message: "Nonce too high, bailing",
totalInclusionFails: totalInclusionFails
})
))
}*/
}
}
catch (e) {
if (!this.executionStopped) {
this.exit();
reject(new Error(JSON.stringify({
blockNumber: targetBlockNumber,
message: e.message,
totalInclusionFails: totalInclusionFails
})));
}
}
});
});
}
}
exports.default = FlashBot;
//# sourceMappingURL=flash-bot.js.map