From 6bb7c8c2462f4ba87e0eba73e9629829cde66df7 Mon Sep 17 00:00:00 2001 From: Utkir S Date: Fri, 8 Dec 2023 15:17:05 +0500 Subject: [PATCH] feat: flashbots (#130) --- README.md | 7 +- lerna.json | 2 +- package.json | 2 +- packages/api/package.json | 8 +- packages/cli/package.json | 14 +- packages/db/package.json | 4 +- packages/executor/package.json | 9 +- packages/executor/src/config.ts | 28 +- .../executor/src/entities/MempoolEntry.ts | 27 + packages/executor/src/entities/interfaces.ts | 8 +- packages/executor/src/executor.ts | 13 +- packages/executor/src/interfaces.ts | 12 +- packages/executor/src/modules/debug.ts | 5 +- packages/executor/src/modules/skandha.ts | 6 +- .../executor/src/services/BundlingService.ts | 572 ------------------ .../src/services/BundlingService/index.ts | 1 + .../services/BundlingService/interfaces.ts | 9 + .../services/BundlingService/relayers/base.ts | 123 ++++ .../BundlingService/relayers/classic.ts | 198 ++++++ .../BundlingService/relayers/flashbots.ts | 178 ++++++ .../BundlingService/relayers/index.ts | 2 + .../src/services/BundlingService/service.ts | 401 ++++++++++++ .../utils/estimateBundleGasLimit.ts | 23 + .../BundlingService/utils/getUserOpHashes.ts | 38 ++ .../services/BundlingService/utils/index.ts | 2 + .../executor/src/services/MempoolService.ts | 40 +- packages/executor/src/utils/index.ts | 8 + packages/monitoring/package.json | 4 +- packages/node/package.json | 16 +- packages/params/package.json | 6 +- packages/types/package.json | 2 +- packages/types/src/api/interfaces.ts | 6 +- .../src/executor/entities/MempoolEntry.ts | 8 + packages/types/src/executor/entities/index.ts | 1 + packages/types/src/executor/index.ts | 2 + packages/utils/package.json | 4 +- yarn.lock | 5 + 37 files changed, 1174 insertions(+), 620 deletions(-) delete mode 100644 packages/executor/src/services/BundlingService.ts create mode 100644 packages/executor/src/services/BundlingService/index.ts create mode 100644 packages/executor/src/services/BundlingService/interfaces.ts create mode 100644 packages/executor/src/services/BundlingService/relayers/base.ts create mode 100644 packages/executor/src/services/BundlingService/relayers/classic.ts create mode 100644 packages/executor/src/services/BundlingService/relayers/flashbots.ts create mode 100644 packages/executor/src/services/BundlingService/relayers/index.ts create mode 100644 packages/executor/src/services/BundlingService/service.ts create mode 100644 packages/executor/src/services/BundlingService/utils/estimateBundleGasLimit.ts create mode 100644 packages/executor/src/services/BundlingService/utils/getUserOpHashes.ts create mode 100644 packages/executor/src/services/BundlingService/utils/index.ts create mode 100644 packages/types/src/executor/entities/MempoolEntry.ts create mode 100644 packages/types/src/executor/entities/index.ts diff --git a/README.md b/README.md index 2c7afa98..9893f3b7 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Or follow the steps below: "throttlingSlack": 10, # optional, see EIP-4337 "banSlack": 50 # optional, see EIP-4337 "minStake": 10000000000, # optional, min stake of an entity (in wei) - "minUnstakeDelay": 1, # optional, min unstake delay of an entity + "minUnstakeDelay": 0, # optional, min unstake delay of an entity "minSignerBalance": 1, # optional, default is 0.1 ETH. If the relayer's balance drops lower than this, it will be selected as a fee collector "multicall": "0xcA11bde05977b3631167028862bE2a173976CA11", # optional, multicall3 contract (see https://github.com/mds1/multicall#multicall3-contract-addresses) "estimationStaticBuffer": 21000, # adds certain amount of gas to callGasLimit on estimation @@ -100,7 +100,10 @@ Or follow the steps below: "paymaster": [], "account": [] }, - "bundleGasLimitMarkup": 25000 # adds some amount of additional gas to a bundle tx + "bundleGasLimitMarkup": 25000, # adds some amount of additional gas to a bundle tx + "relayingMode": "classic"; # optional, allows to switch to Flashbots Builder api if set to "flashbots", see packages/executor/src/interfaces.ts for more + "bundleInterval": 10000, # bundle creation interval + "bundleSize": 4 # max size of a bundle, 4 userops by default } } } diff --git a/lerna.json b/lerna.json index ebcfacf0..ace792aa 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,7 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "stream": "true", "command": { "version": { diff --git a/package.json b/package.json index 99baa2fd..0e07ddba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "root", "private": true, - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "engines": { "node": ">=18.0.0" }, diff --git a/packages/api/package.json b/packages/api/package.json index e041f6c7..73dd5d15 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "api", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "The API module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -35,13 +35,13 @@ "class-transformer": "0.5.1", "class-validator": "0.14.0", "ethers": "5.7.2", - "executor": "^1.0.26-alpha", + "executor": "^1.0.27-alpha", "fastify": "4.14.1", - "monitoring": "^1.0.26-alpha", + "monitoring": "^1.0.27-alpha", "pino": "8.11.0", "pino-pretty": "10.0.0", "reflect-metadata": "0.1.13", - "types": "^1.0.26-alpha" + "types": "^1.0.27-alpha" }, "devDependencies": { "@types/connect": "3.4.35" diff --git a/packages/cli/package.json b/packages/cli/package.json index 0cf37b14..de68c0b4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "cli", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "> TODO: description", "author": "zincoshine ", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -38,15 +38,15 @@ "@libp2p/peer-id-factory": "2.0.1", "@libp2p/prometheus-metrics": "1.1.3", "@multiformats/multiaddr": "12.1.3", - "api": "^1.0.26-alpha", - "db": "^1.0.26-alpha", - "executor": "^1.0.26-alpha", + "api": "^1.0.27-alpha", + "db": "^1.0.27-alpha", + "executor": "^1.0.27-alpha", "find-up": "5.0.0", "got": "12.5.3", "js-yaml": "4.1.0", - "monitoring": "^1.0.26-alpha", - "node": "^1.0.26-alpha", - "types": "^1.0.26-alpha", + "monitoring": "^1.0.27-alpha", + "node": "^1.0.27-alpha", + "types": "^1.0.27-alpha", "yargs": "17.6.2" }, "devDependencies": { diff --git a/packages/db/package.json b/packages/db/package.json index 44dbc3ee..2c2e97f7 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "db", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "The DB module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://github.com/etherspot/etherspot-bundler#readme", @@ -33,7 +33,7 @@ "dependencies": { "@chainsafe/ssz": "0.10.1", "@farcaster/rocksdb": "5.5.0", - "types": "^1.0.26-alpha" + "types": "^1.0.27-alpha" }, "devDependencies": { "@types/rocksdb": "3.0.1", diff --git a/packages/executor/package.json b/packages/executor/package.json index f31933ee..91020017 100644 --- a/packages/executor/package.json +++ b/packages/executor/package.json @@ -1,6 +1,6 @@ { "name": "executor", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "The Relayer module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -31,10 +31,11 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { + "@flashbots/ethers-provider-bundle": "0.6.2", "async-mutex": "0.4.0", "ethers": "5.7.2", - "monitoring": "^1.0.26-alpha", - "params": "^1.0.26-alpha", - "types": "^1.0.26-alpha" + "monitoring": "^1.0.27-alpha", + "params": "^1.0.27-alpha", + "types": "^1.0.27-alpha" } } diff --git a/packages/executor/src/config.ts b/packages/executor/src/config.ts index 01ca3b63..cc780e01 100644 --- a/packages/executor/src/config.ts +++ b/packages/executor/src/config.ts @@ -1,7 +1,7 @@ // TODO: create a new package "config" instead of this file and refactor import { BigNumber, Wallet, providers, utils } from "ethers"; import { NetworkName } from "types/lib"; -import { IEntity } from "types/lib/executor"; +import { IEntity, RelayingMode } from "types/lib/executor"; import { getAddress } from "ethers/lib/utils"; import { BundlerConfig, @@ -260,6 +260,27 @@ export class Config { conf.bundleGasLimitMarkup || bundlerDefaultConfigs.bundleGasLimitMarkup ) ); + conf.relayingMode = fromEnvVar( + network, + "RELAYING_MODE", + conf.relayingMode || bundlerDefaultConfigs.relayingMode + ) as RelayingMode; + + conf.bundleInterval = Number( + fromEnvVar( + network, + "BUNDLE_INTERVAL", + conf.bundleInterval || bundlerDefaultConfigs.bundleInterval + ) + ); + + conf.bundleSize = Number( + fromEnvVar( + network, + "BUNDLE_SIZE", + conf.bundleSize || bundlerDefaultConfigs.bundleSize + ) + ); // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!conf.whitelistedEntities) { @@ -297,7 +318,7 @@ const bundlerDefaultConfigs: BundlerConfig = { throttlingSlack: 10, banSlack: 50, minStake: utils.parseEther("0.01"), - minUnstakeDelay: 1, + minUnstakeDelay: 0, minSignerBalance: utils.parseEther("0.1"), multicall: "0xcA11bde05977b3631167028862bE2a173976CA11", // default multicall address estimationStaticBuffer: 35000, @@ -313,6 +334,9 @@ const bundlerDefaultConfigs: BundlerConfig = { useropsTTL: 300, // 5 minutes whitelistedEntities: { paymaster: [], account: [], factory: [] }, bundleGasLimitMarkup: 25000, + bundleInterval: 10000, // 10 seconds + bundleSize: 4, // max size of bundle (in terms of user ops) + relayingMode: "classic", }; const NETWORKS_ENV = (): string[] | undefined => { diff --git a/packages/executor/src/entities/MempoolEntry.ts b/packages/executor/src/entities/MempoolEntry.ts index d250cd3e..68bea07b 100644 --- a/packages/executor/src/entities/MempoolEntry.ts +++ b/packages/executor/src/entities/MempoolEntry.ts @@ -3,6 +3,7 @@ import { getAddress, hexValue } from "ethers/lib/utils"; import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; import RpcError from "types/lib/api/errors/rpc-error"; import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint"; +import { MempoolEntryStatus } from "types/lib/executor"; import { now } from "../utils"; import { IMempoolEntry, MempoolEntrySerialized } from "./interfaces"; @@ -16,7 +17,10 @@ export class MempoolEntry implements IMempoolEntry { paymaster?: string; lastUpdatedTime: number; userOpHash: string; + status: MempoolEntryStatus; hash?: string; // keccak256 of all referenced contracts + transaction?: string; // hash of a submitted bundle + submitAttempts: number; constructor({ chainId, @@ -29,6 +33,9 @@ export class MempoolEntry implements IMempoolEntry { userOpHash, hash, lastUpdatedTime, + status, + transaction, + submitAttempts, }: { chainId: number; userOp: UserOperationStruct; @@ -40,6 +47,9 @@ export class MempoolEntry implements IMempoolEntry { userOpHash: string; hash?: string | undefined; lastUpdatedTime?: number | undefined; + status?: MempoolEntryStatus | undefined; + transaction?: string | undefined; + submitAttempts?: number | undefined; }) { this.chainId = chainId; this.userOp = userOp; @@ -51,9 +61,23 @@ export class MempoolEntry implements IMempoolEntry { this.paymaster = paymaster; this.hash = hash; this.lastUpdatedTime = lastUpdatedTime ?? now(); + this.status = status ?? MempoolEntryStatus.New; + this.transaction = transaction; + this.submitAttempts = submitAttempts ?? 0; this.validateAndTransformUserOp(); } + /** + * Set status of an entry + * If status is Pending, transaction hash is required + */ + setStatus(status: MempoolEntryStatus, transaction?: string): void { + this.status = status; + if (transaction) { + this.transaction = transaction; + } + } + /** * To replace an entry, a new entry must have at least 10% higher maxPriorityFeePerGas * and 10% higher maxPriorityFeePerGas than the existingEntry @@ -159,6 +183,9 @@ export class MempoolEntry implements IMempoolEntry { hash: this.hash, userOpHash: this.userOpHash, lastUpdatedTime: this.lastUpdatedTime, + transaction: this.transaction, + submitAttempts: this.submitAttempts, + status: this.status, }; } } diff --git a/packages/executor/src/entities/interfaces.ts b/packages/executor/src/entities/interfaces.ts index ca9bd743..553438a6 100644 --- a/packages/executor/src/entities/interfaces.ts +++ b/packages/executor/src/entities/interfaces.ts @@ -1,5 +1,5 @@ import { BigNumberish, BytesLike } from "ethers"; -import { ReputationStatus } from "types/lib/executor"; +import { MempoolEntryStatus, ReputationStatus } from "types/lib/executor"; import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint"; export interface IMempoolEntry { @@ -13,6 +13,9 @@ export interface IMempoolEntry { userOpHash: string; lastUpdatedTime: number; hash?: string; + status: MempoolEntryStatus; + transaction?: string; + submitAttempts: number; } export interface MempoolEntrySerialized { @@ -37,6 +40,9 @@ export interface MempoolEntrySerialized { userOpHash: string; hash: string | undefined; lastUpdatedTime: number; + transaction: string | undefined; + submitAttempts: number; + status: MempoolEntryStatus; } export interface IReputationEntry { diff --git a/packages/executor/src/executor.ts b/packages/executor/src/executor.ts index 9a856c56..f69a52d6 100644 --- a/packages/executor/src/executor.ts +++ b/packages/executor/src/executor.ts @@ -102,7 +102,8 @@ export class Executor { this.reputationService, this.config, this.logger, - this.metrics + this.metrics, + this.networkConfig.relayingMode ); this.eventsService = new EventsService( this.chainId, @@ -150,7 +151,15 @@ export class Executor { if (this.config.testingMode || options.bundlingMode == "manual") { this.bundlingService.setBundlingMode("manual"); - this.logger.info(`${this.networkName}: set to manual bundling mode`); + this.logger.info(`${this.networkName}: [X] MANUAL BUNDLING`); + } + + if (this.networkConfig.relayingMode === "flashbots") { + if (!this.networkConfig.rpcEndpointSubmit) + throw Error( + "If you want to use Flashbots Builder API, please set API url in 'rpcEndpointSubmit' in config file" + ); + this.logger.info(`${this.networkName}: [X] FLASHBOTS BUIDLER API`); } if (this.networkConfig.conditionalTransactions) { diff --git a/packages/executor/src/interfaces.ts b/packages/executor/src/interfaces.ts index 71acaa6a..b62040fb 100644 --- a/packages/executor/src/interfaces.ts +++ b/packages/executor/src/interfaces.ts @@ -1,6 +1,6 @@ import { BigNumber, BigNumberish, BytesLike } from "ethers"; import { NetworkName } from "types/lib"; -import { IWhitelistedEntities } from "types/lib/executor"; +import { IWhitelistedEntities, RelayingMode } from "types/lib/executor"; import { Executor } from "./executor"; import { MempoolEntry } from "./entities/MempoolEntry"; @@ -131,6 +131,16 @@ export interface NetworkConfig { whitelistedEntities: IWhitelistedEntities; // adds some amount of gas to a estimated bundle bundleGasLimitMarkup: number; + // relaying mode: via Flashbots Builder API or classic relaying + // default is "classic" + // if flashbots is used, "rpcEndpointSubmit" must be set + relayingMode: RelayingMode; + // Interval of bundling + // default is 10 seconds + bundleInterval: number; + // max bundle size in terms of user ops + // default is 4 + bundleSize: number; } export type BundlerConfig = Omit< diff --git a/packages/executor/src/modules/debug.ts b/packages/executor/src/modules/debug.ts index bae4602e..8e650832 100644 --- a/packages/executor/src/modules/debug.ts +++ b/packages/executor/src/modules/debug.ts @@ -6,6 +6,7 @@ import { IEntryPoint__factory, StakeManager__factory, } from "types/lib/executor/contracts"; +import { MempoolEntryStatus } from "types/lib/executor"; import { BundlingService, MempoolService, @@ -65,7 +66,9 @@ export class Debug { */ async dumpMempool(): Promise { const entries = await this.mempoolService.dump(); - return entries.map((entry) => entry.userOp); + return entries + .filter((entry) => entry.status === MempoolEntryStatus.New) + .map((entry) => entry.userOp); } /** diff --git a/packages/executor/src/modules/skandha.ts b/packages/executor/src/modules/skandha.ts index 146dc1af..86ffe809 100644 --- a/packages/executor/src/modules/skandha.ts +++ b/packages/executor/src/modules/skandha.ts @@ -75,7 +75,6 @@ export class Skandha { return { chainId: this.chainId, flags: { - unsafeMode: this.config.unsafeMode, testingMode: this.config.testingMode, redirectRpc: this.config.redirectRpc, }, @@ -115,6 +114,11 @@ export class Skandha { eip2930: this.networkConfig.eip2930, useropsTTL: this.networkConfig.useropsTTL, whitelistedEntities: this.networkConfig.whitelistedEntities, + bundleGasLimitMarkup: this.networkConfig.bundleGasLimitMarkup, + relayingMode: this.networkConfig.relayingMode, + bundleInterval: this.networkConfig.bundleInterval, + bundleSize: this.networkConfig.bundleSize, + minUnstakeDelay: this.networkConfig.minUnstakeDelay, }; } diff --git a/packages/executor/src/services/BundlingService.ts b/packages/executor/src/services/BundlingService.ts deleted file mode 100644 index e45be105..00000000 --- a/packages/executor/src/services/BundlingService.ts +++ /dev/null @@ -1,572 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { BigNumber, ethers, providers } from "ethers"; -import { NetworkName, Logger } from "types/lib"; -import { IEntryPoint__factory } from "types/lib/executor/contracts/factories"; -import { Mutex } from "async-mutex"; -import { SendBundleReturn, ReputationStatus } from "types/lib/executor"; -import { IMulticall3__factory } from "types/lib/executor/contracts/factories/IMulticall3__factory"; -import { GasPriceMarkupOne, chainsWithoutEIP1559 } from "params/lib"; -import { IEntryPoint } from "types/lib/executor/contracts"; -import { getGasFee } from "params/lib"; -import { IGetGasFeeResult } from "params/lib/gas-price-oracles/oracles"; -import { AccessList } from "ethers/lib/utils"; -import { PerChainMetrics } from "monitoring/lib"; -import { getAddr, now } from "../utils"; -import { MempoolEntry } from "../entities/MempoolEntry"; -import { Config } from "../config"; -import { - Bundle, - BundlingMode, - NetworkConfig, - UserOpValidationResult, -} from "../interfaces"; -import { mergeStorageMap } from "../utils/mergeStorageMap"; -import { ReputationService } from "./ReputationService"; -import { UserOpValidationService } from "./UserOpValidation"; -import { MempoolService } from "./MempoolService"; - -export class BundlingService { - private mutex: Mutex; - private bundlingMode: BundlingMode; - private autoBundlingInterval: number; - private autoBundlingCron?: NodeJS.Timer; - private maxMempoolSize: number; - private networkConfig: NetworkConfig; - - constructor( - private chainId: number, - private network: NetworkName, - private provider: providers.JsonRpcProvider, - private mempoolService: MempoolService, - private userOpValidationService: UserOpValidationService, - private reputationService: ReputationService, - private config: Config, - private logger: Logger, - private metrics: PerChainMetrics | null - ) { - this.networkConfig = config.getNetworkConfig(network)!; - this.mutex = new Mutex(); - this.bundlingMode = "auto"; - this.autoBundlingInterval = 15 * 1000; - this.maxMempoolSize = 2; - this.restartCron(); - } - - async sendNextBundle(): Promise { - return await this.mutex.runExclusive(async () => { - const entries = await this.mempoolService.getSortedOps(); - if (!entries.length) { - return null; - } - this.logger.debug("sendNextBundle"); - const gasFee = await getGasFee( - this.chainId, - this.provider, - this.networkConfig.etherscanApiKey - ); - if ( - gasFee.gasPrice == undefined && - gasFee.maxFeePerGas == undefined && - gasFee.maxPriorityFeePerGas == undefined - ) { - this.logger.debug("Could not fetch gas prices..."); - return null; - } - const bundle = await this.createBundle(gasFee); - if (bundle.entries.length == 0) { - this.logger.debug("sendNextBundle - no bundle"); - return null; - } - return await this.sendBundle(bundle); - }); - } - - async sendBundle(bundle: Bundle): Promise { - const { entries, storageMap } = bundle; - if (!bundle.entries.length) { - return null; - } - const entryPoint = entries[0]!.entryPoint; - const entryPointContract = IEntryPoint__factory.connect( - entryPoint, - this.provider - ); - const wallet = this.config.getRelayer(this.network)!; - const beneficiary = await this.selectBeneficiary(); - try { - const txRequest = entryPointContract.interface.encodeFunctionData( - "handleOps", - [entries.map((entry) => entry.userOp), beneficiary] - ); - - const transaction: ethers.providers.TransactionRequest = { - to: entryPoint, - data: txRequest, - type: 2, - maxPriorityFeePerGas: bundle.maxPriorityFeePerGas, - maxFeePerGas: bundle.maxFeePerGas, - }; - if (this.networkConfig.eip2930) { - const { storageMap } = bundle; - const addresses = Object.keys(storageMap); - if (addresses.length) { - const accessList: AccessList = []; - for (const address of addresses) { - const storageKeys = storageMap[address]; - if (typeof storageKeys == "object") { - accessList.push({ - address, - storageKeys: Object.keys(storageKeys), - }); - } - } - transaction.accessList = accessList; - } - } - - if (chainsWithoutEIP1559.some((chainId) => chainId === this.chainId)) { - transaction.gasPrice = bundle.maxFeePerGas; - delete transaction.maxPriorityFeePerGas; - delete transaction.maxFeePerGas; - delete transaction.type; - delete transaction.accessList; - } - - const gasLimit = await this.estimateBundleGas(entries); - const tx = { - ...transaction, - gasLimit, - chainId: this.provider._network.chainId, - nonce: await wallet.getTransactionCount(), - }; - - let txHash: string; - // geth-dev doesn't support signTransaction - if (!this.config.testingMode) { - // check for execution revert - try { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { gasLimit, ...txWithoutGasLimit } = tx; - // some chains, like Bifrost, don't allow setting gasLimit in estimateGas - await wallet.estimateGas(txWithoutGasLimit); - } catch (err) { - this.logger.error(err); - for (const entry of entries) { - await this.mempoolService.remove(entry); - } - return null; - } - const signedRawTx = await wallet.signTransaction(tx); - - const method = !this.networkConfig.conditionalTransactions - ? "eth_sendRawTransaction" - : "eth_sendRawTransactionConditional"; - const params = !this.networkConfig.conditionalTransactions - ? [signedRawTx] - : [signedRawTx, { knownAccounts: storageMap }]; - - this.logger.debug({ - method, - ...tx, - params, - }); - - if (this.networkConfig.rpcEndpointSubmit) { - this.logger.debug("Sending to a separate rpc"); - const provider = new ethers.providers.JsonRpcProvider( - this.networkConfig.rpcEndpointSubmit - ); - txHash = await provider.send(method, params); - } else { - txHash = await this.provider.send(method, params); - } - - this.logger.debug(`Sent new bundle ${txHash}`); - } else { - const resp = await wallet.sendTransaction(tx); - txHash = resp.hash; - } - - for (const entry of entries) { - await this.mempoolService.remove(entry); - } - - const userOpHashes = await this.getUserOpHashes( - entryPointContract, - entries - ); - this.logger.debug(`User op hashes ${userOpHashes}`); - - // metrics - if (this.metrics) { - this.metrics.useropsSubmitted.inc(bundle.entries.length); - bundle.entries.forEach((entry) => { - this.metrics!.useropsTimeToProcess.observe( - now() - entry.lastUpdatedTime - ); - }); - } - - return { - transactionHash: txHash, - userOpHashes: userOpHashes, - }; - } catch (err: any) { - if (err.errorName !== "FailedOp") { - this.logger.error(`Failed handleOps, but non-FailedOp error ${err}`); - return null; - } - const { index, paymaster, reason } = err.errorArgs; - const entry = entries[index]; - if (paymaster !== ethers.constants.AddressZero) { - await this.reputationService.crashedHandleOps(paymaster); - } else if (typeof reason === "string" && reason.startsWith("AA1")) { - const factory = getAddr(entry?.userOp.initCode); - if (factory) { - await this.reputationService.crashedHandleOps(factory); - } - } else { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (entry) { - await this.mempoolService.remove(entry); - this.logger.error(`Failed handleOps sender=${entry.userOp.sender}`); - } - } - return null; - } - } - - async createBundle(gasFee: IGetGasFeeResult): Promise { - // TODO: support multiple entry points - // filter bundles by entry points - const entries = await this.mempoolService.getSortedOps(); - const bundle: Bundle = { - storageMap: {}, - entries: [], - maxFeePerGas: BigNumber.from(0), - maxPriorityFeePerGas: BigNumber.from(0), - }; - - const paymasterDeposit: { [key: string]: BigNumber } = {}; - const stakedEntityCount: { [key: string]: number } = {}; - const senders = new Set(); - const knownSenders = entries.map((it) => { - return it.userOp.sender.toLowerCase(); - }); - - for (const entry of entries) { - // validate gas prices if enabled - if (this.networkConfig.enforceGasPrice) { - let { maxPriorityFeePerGas, maxFeePerGas } = gasFee; - const { enforceGasPriceThreshold } = this.networkConfig; - if (chainsWithoutEIP1559.some((chainId) => chainId === this.chainId)) { - maxFeePerGas = maxPriorityFeePerGas = gasFee.gasPrice; - } - // userop max fee per gas = userop.maxFee * (100 + threshold) / 100; - const userOpMaxFeePerGas = BigNumber.from(entry.userOp.maxFeePerGas) - .mul(GasPriceMarkupOne.add(enforceGasPriceThreshold)) - .div(GasPriceMarkupOne); - // userop priority fee per gas = userop.priorityFee * (100 + threshold) / 100; - const userOpmaxPriorityFeePerGas = BigNumber.from( - entry.userOp.maxPriorityFeePerGas - ) - .mul(GasPriceMarkupOne.add(enforceGasPriceThreshold)) - .div(GasPriceMarkupOne); - if ( - userOpMaxFeePerGas.lt(maxFeePerGas!) || - userOpmaxPriorityFeePerGas.lt(maxPriorityFeePerGas!) - ) { - this.logger.debug( - { - sender: entry.userOp.sender, - nonce: entry.userOp.nonce.toString(), - userOpMaxFeePerGas: userOpMaxFeePerGas.toString(), - userOpmaxPriorityFeePerGas: userOpmaxPriorityFeePerGas.toString(), - maxPriorityFeePerGas: maxPriorityFeePerGas!.toString(), - maxFeePerGas: maxFeePerGas!.toString(), - }, - "Skipping user op with low gas price" - ); - continue; - } - } - - const paymaster = getAddr(entry.userOp.paymasterAndData); - const factory = getAddr(entry.userOp.initCode); - - // validate Paymaster - if (paymaster) { - const paymasterStatus = await this.reputationService.getStatus( - paymaster - ); - if (paymasterStatus === ReputationStatus.BANNED) { - await this.mempoolService.remove(entry); - continue; - } else if ( - paymasterStatus === ReputationStatus.THROTTLED || - (stakedEntityCount[paymaster] ?? 0) > 1 - ) { - this.logger.debug( - { - sender: entry.userOp.sender, - nonce: entry.userOp.nonce, - paymaster, - }, - "skipping throttled paymaster" - ); - continue; - } - } - - // validate Factory - if (factory) { - const deployerStatus = await this.reputationService.getStatus(factory); - if (deployerStatus === ReputationStatus.BANNED) { - await this.mempoolService.remove(entry); - continue; - } else if ( - deployerStatus === ReputationStatus.THROTTLED || - (stakedEntityCount[factory] ?? 0) > 1 - ) { - this.logger.debug( - { - sender: entry.userOp.sender, - nonce: entry.userOp.nonce, - factory, - }, - "skipping throttled factory" - ); - continue; - } - } - - if (senders.has(entry.userOp.sender)) { - this.logger.debug( - { sender: entry.userOp.sender, nonce: entry.userOp.nonce }, - "skipping already included sender" - ); - continue; - } - - let validationResult: UserOpValidationResult; - try { - validationResult = - await this.userOpValidationService.simulateValidation( - entry.userOp, - entry.entryPoint, - entry.hash - ); - } catch (e: any) { - this.logger.debug(`failed 2nd validation: ${e.message}`); - await this.mempoolService.remove(entry); - continue; - } - - // Check if userOp is trying to access storage of another userop - if (validationResult.storageMap) { - const sender = entry.userOp.sender.toLowerCase(); - const conflictingSender = Object.keys(validationResult.storageMap) - .map((address) => address.toLowerCase()) - .find((address) => { - return address !== sender && knownSenders.includes(address); - }); - if (conflictingSender) { - this.logger.debug( - `UserOperation from ${entry.userOp.sender} sender accessed a storage of another known sender ${conflictingSender}` - ); - continue; - } - } - - // TODO: add total gas cap - const entryPointContract = IEntryPoint__factory.connect( - entry.entryPoint, - this.provider - ); - if (paymaster) { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!paymasterDeposit[paymaster]) { - paymasterDeposit[paymaster] = await entryPointContract.balanceOf( - paymaster - ); - } - if ( - paymasterDeposit[paymaster]?.lt(validationResult.returnInfo.prefund) - ) { - // not enough balance in paymaster to pay for all UserOps - // (but it passed validation, so it can sponsor them separately - continue; - } - stakedEntityCount[paymaster] = (stakedEntityCount[paymaster] ?? 0) + 1; - paymasterDeposit[paymaster] = BigNumber.from( - paymasterDeposit[paymaster]?.sub(validationResult.returnInfo.prefund) - ); - } - - if (factory) { - stakedEntityCount[factory] = (stakedEntityCount[factory] ?? 0) + 1; - } - - senders.add(entry.userOp.sender); - - this.metrics?.useropsAttempted.inc(); - - if ( - (this.networkConfig.conditionalTransactions || - this.networkConfig.eip2930) && - validationResult.storageMap - ) { - if (BigNumber.from(entry.userOp.nonce).gt(0)) { - const { storageHash } = await this.provider.send("eth_getProof", [ - entry.userOp.sender, - [], - "latest", - ]); - bundle.storageMap[entry.userOp.sender.toLowerCase()] = storageHash; - } - mergeStorageMap(bundle.storageMap, validationResult.storageMap); - } - bundle.entries.push(entry); - - const { maxFeePerGas, maxPriorityFeePerGas } = bundle; - bundle.maxFeePerGas = maxFeePerGas.add(entry.userOp.maxFeePerGas); - bundle.maxPriorityFeePerGas = maxPriorityFeePerGas.add( - entry.userOp.maxPriorityFeePerGas - ); - } - - // skip gas fee protection on Fuse - if (this.provider.network.chainId == 122) { - bundle.maxFeePerGas = BigNumber.from(gasFee.maxFeePerGas); - bundle.maxPriorityFeePerGas = BigNumber.from(gasFee.maxPriorityFeePerGas); - return bundle; - } - - if (bundle.entries.length > 1) { - // average of userops - bundle.maxFeePerGas = bundle.maxFeePerGas.div(bundle.entries.length); - bundle.maxPriorityFeePerGas = bundle.maxPriorityFeePerGas.div( - bundle.entries.length - ); - } - - // if onchain fee is less than userops fee, use onchain fee - if ( - bundle.maxFeePerGas.gt(gasFee.maxFeePerGas ?? gasFee.gasPrice!) && - bundle.maxPriorityFeePerGas.gt(gasFee.maxPriorityFeePerGas!) - ) { - bundle.maxFeePerGas = BigNumber.from( - gasFee.maxFeePerGas ?? gasFee.gasPrice! - ); - bundle.maxPriorityFeePerGas = BigNumber.from( - gasFee.maxPriorityFeePerGas! - ); - } - - return bundle; - } - - setBundlingMode(mode: BundlingMode): void { - this.bundlingMode = mode; - this.restartCron(); - } - - setBundlingInverval(interval: number): void { - if (interval > 1) { - this.autoBundlingInterval = interval * 1000; - this.restartCron(); - } - } - - setMaxMempoolSize(size: number): void { - this.maxMempoolSize = size; - this.restartCron(); - } - - private restartCron(): void { - if (this.autoBundlingCron) { - clearInterval(this.autoBundlingCron); - } - if (this.bundlingMode !== "auto") { - return; - } - this.autoBundlingCron = setInterval(() => { - void this.tryBundle(); - }, this.autoBundlingInterval); - } - - // sends new bundle if force = true or there is enough entries in mempool - private async tryBundle(force = true): Promise { - if (!force) { - const count = await this.mempoolService.count(); - if (count < this.maxMempoolSize) { - return; - } - } - await this.sendNextBundle(); - } - - /** - * determine who should receive the proceedings of the request. - * if signer's balance is too low, send it to signer. otherwise, send to configured beneficiary. - */ - private async selectBeneficiary(): Promise { - const config = this.config.getNetworkConfig(this.network); - let beneficiary = this.config.getBeneficiary(this.network); - const signer = this.config.getRelayer(this.network); - const signerAddress = await signer!.getAddress(); - const currentBalance = await this.provider.getBalance(signerAddress); - - if (currentBalance.lte(config!.minSignerBalance) || !beneficiary) { - beneficiary = signerAddress; - this.logger.info( - `low balance on ${signerAddress}. using it as beneficiary` - ); - } - return beneficiary; - } - - async getUserOpHashes( - entryPoint: IEntryPoint, - userOps: MempoolEntry[] - ): Promise { - try { - const config = this.config.getNetworkConfig(this.network); - const multicall = IMulticall3__factory.connect( - config!.multicall, - this.provider - ); - const callDatas = userOps.map((op) => - entryPoint.interface.encodeFunctionData("getUserOpHash", [op.userOp]) - ); - const result = await multicall.callStatic.aggregate3( - callDatas.map((data) => ({ - target: entryPoint.address, - callData: data, - allowFailure: false, - })) - ); - return result.map((call) => call.returnData); - } catch (err) { - return []; - } - } - - private async estimateBundleGas(bundle: MempoolEntry[]): Promise { - let gasLimit = BigNumber.from(this.networkConfig.bundleGasLimitMarkup); - for (const { userOp } of bundle) { - gasLimit = BigNumber.from(userOp.verificationGasLimit) - .mul(3) - .add(userOp.preVerificationGas) - .add(userOp.callGasLimit) - .mul(11) - .div(10) - .add(gasLimit); - } - if (gasLimit.lt(1e5)) { - // gasLimit should at least be 1e5 to pass test in test-executor - gasLimit = BigNumber.from(1e5); - } - return gasLimit; - } -} diff --git a/packages/executor/src/services/BundlingService/index.ts b/packages/executor/src/services/BundlingService/index.ts new file mode 100644 index 00000000..6261f896 --- /dev/null +++ b/packages/executor/src/services/BundlingService/index.ts @@ -0,0 +1 @@ +export * from "./service"; diff --git a/packages/executor/src/services/BundlingService/interfaces.ts b/packages/executor/src/services/BundlingService/interfaces.ts new file mode 100644 index 00000000..cfc61463 --- /dev/null +++ b/packages/executor/src/services/BundlingService/interfaces.ts @@ -0,0 +1,9 @@ +import { Wallet, providers } from "ethers"; +import { Bundle } from "../../interfaces"; + +export type Relayer = Wallet | providers.JsonRpcSigner; + +export interface IRelayingMode { + isLocked(): boolean; + sendBundle(bundle: Bundle, beneficiary: string): Promise; +} diff --git a/packages/executor/src/services/BundlingService/relayers/base.ts b/packages/executor/src/services/BundlingService/relayers/base.ts new file mode 100644 index 00000000..a641c26f --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/base.ts @@ -0,0 +1,123 @@ +import { Mutex } from "async-mutex"; +import { constants, providers, utils } from "ethers"; +import { Logger, NetworkName } from "types/lib"; +import { PerChainMetrics } from "monitoring/lib"; +import { Config } from "../../../config"; +import { Bundle, NetworkConfig } from "../../../interfaces"; +import { IRelayingMode, Relayer } from "../interfaces"; +import { MempoolEntry } from "../../../entities/MempoolEntry"; +import { getAddr, now } from "../../../utils"; +import { MempoolService } from "../../MempoolService"; +import { ReputationService } from "../../ReputationService"; + +const WAIT_FOR_TX_MAX_RETRIES = 3; // 3 blocks + +export abstract class BaseRelayer implements IRelayingMode { + protected relayers: Relayer[]; + protected mutexes: Mutex[]; + + constructor( + protected logger: Logger, + protected chainId: number, + protected network: NetworkName, + protected provider: providers.JsonRpcProvider, + protected config: Config, + protected networkConfig: NetworkConfig, + protected mempoolService: MempoolService, + protected reputationService: ReputationService, + protected metrics: PerChainMetrics | null + ) { + const relayer = this.config.getRelayer(this.network); + if (!relayer) throw new Error("Relayer is not set"); + this.relayers = [relayer]; + this.mutexes = this.relayers.map(() => new Mutex()); + } + + isLocked(): boolean { + return this.mutexes.every((mutex) => mutex.isLocked()); + } + + sendBundle(_bundle: Bundle, _beneficiary: string): Promise { + throw new Error("Method not implemented."); + } + + /** + * waits for transaction + * @param hash transaction hash + * @returns false if transaction reverted + */ + protected async waitForTransaction(hash: string): Promise { + if (!utils.isHexString(hash)) return false; + let retries = 0; + return new Promise((resolve, reject) => { + const interval = setInterval(async () => { + if (retries >= WAIT_FOR_TX_MAX_RETRIES) reject(false); + retries++; + const response = await this.provider.getTransaction(hash); + if (response != null) { + clearInterval(interval); + try { + await response.wait(0); + resolve(true); + } catch (err) { + reject(err); + } + } + }, 1000); + }); + } + + protected getAvailableRelayerIndex(): number | null { + const index = this.mutexes.findIndex((mutex) => !mutex.isLocked()); + if (index === -1) { + return null; + } + return index; + } + + protected async handleUserOpFail( + entries: MempoolEntry[], + err: any + ): Promise { + if (err.errorName !== "FailedOp") { + this.logger.error( + `Failed handleOps, but non-FailedOp error ${JSON.stringify( + err, + undefined, + 2 + )}` + ); + return; + } + const { index, paymaster, reason } = err.errorArgs; + const failedEntry = entries[index]; + if (paymaster !== constants.AddressZero) { + await this.reputationService.crashedHandleOps(paymaster); + } else if (typeof reason === "string" && reason.startsWith("AA1")) { + const factory = getAddr(failedEntry?.userOp.initCode); + if (factory) { + await this.reputationService.crashedHandleOps(factory); + } + } else { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (failedEntry) { + await this.mempoolService.remove(failedEntry); + this.logger.error( + `Failed handleOps sender=${failedEntry.userOp.sender}` + ); + } + } + } + + // metrics + protected reportSubmittedUserops(txHash: string, bundle: Bundle): void { + if (txHash && this.metrics) { + this.metrics.useropsSubmitted.inc(bundle.entries.length); + bundle.entries.forEach((entry) => { + this.metrics!.useropsTimeToProcess.observe( + now() - entry.lastUpdatedTime + ); + }); + } + } +} diff --git a/packages/executor/src/services/BundlingService/relayers/classic.ts b/packages/executor/src/services/BundlingService/relayers/classic.ts new file mode 100644 index 00000000..d41fae52 --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/classic.ts @@ -0,0 +1,198 @@ +import { providers } from "ethers"; +import { NetworkName, Logger } from "types/lib"; +import { PerChainMetrics } from "monitoring/lib"; +import { IEntryPoint__factory } from "types/lib/executor/contracts"; +import { chainsWithoutEIP1559 } from "params/lib"; +import { AccessList } from "ethers/lib/utils"; +import { MempoolEntryStatus } from "types/lib/executor"; +import { Relayer } from "../interfaces"; +import { Config } from "../../../config"; +import { Bundle, NetworkConfig, StorageMap } from "../../../interfaces"; +import { MempoolService } from "../../MempoolService"; +import { estimateBundleGasLimit } from "../utils"; +import { ReputationService } from "../../ReputationService"; +import { BaseRelayer } from "./base"; + +export class ClassicRelayer extends BaseRelayer { + constructor( + logger: Logger, + chainId: number, + network: NetworkName, + provider: providers.JsonRpcProvider, + config: Config, + networkConfig: NetworkConfig, + mempoolService: MempoolService, + reputationService: ReputationService, + metrics: PerChainMetrics | null + ) { + super( + logger, + chainId, + network, + provider, + config, + networkConfig, + mempoolService, + reputationService, + metrics + ); + } + + async sendBundle(bundle: Bundle, beneficiary: string): Promise { + const availableIndex = this.getAvailableRelayerIndex(); + if (availableIndex == null) return; + const relayer = this.relayers[availableIndex]; + const mutex = this.mutexes[availableIndex]; + + const { entries, storageMap } = bundle; + if (!bundle.entries.length) return; + + await mutex.runExclusive(async (): Promise => { + const entryPoint = entries[0]!.entryPoint; + const entryPointContract = IEntryPoint__factory.connect( + entryPoint, + this.provider + ); + + const txRequest = entryPointContract.interface.encodeFunctionData( + "handleOps", + [entries.map((entry) => entry.userOp), beneficiary] + ); + + const transactionRequest: providers.TransactionRequest = { + to: entryPoint, + data: txRequest, + type: 2, + maxPriorityFeePerGas: bundle.maxPriorityFeePerGas, + maxFeePerGas: bundle.maxFeePerGas, + }; + + if (this.networkConfig.eip2930) { + const { storageMap } = bundle; + const addresses = Object.keys(storageMap); + if (addresses.length) { + const accessList: AccessList = []; + for (const address of addresses) { + const storageKeys = storageMap[address]; + if (typeof storageKeys == "object") { + accessList.push({ + address, + storageKeys: Object.keys(storageKeys), + }); + } + } + transactionRequest.accessList = accessList; + } + } + + if ( + chainsWithoutEIP1559.some((chainId: number) => chainId === this.chainId) + ) { + transactionRequest.gasPrice = bundle.maxFeePerGas; + delete transactionRequest.maxPriorityFeePerGas; + delete transactionRequest.maxFeePerGas; + delete transactionRequest.type; + delete transactionRequest.accessList; + } + + const transaction = { + ...transactionRequest, + gasLimit: estimateBundleGasLimit( + this.networkConfig.bundleGasLimitMarkup, + bundle.entries + ), + chainId: this.provider._network.chainId, + nonce: await relayer.getTransactionCount(), + }; + + // geth-dev's jsonRpcSigner doesn't support signTransaction + if (!this.config.testingMode) { + // check for execution revert + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { gasLimit, ...txWithoutGasLimit } = transactionRequest; + // some chains, like Bifrost, don't allow setting gasLimit in estimateGas + await relayer.estimateGas(txWithoutGasLimit); + } catch (err) { + this.logger.error(err); + await this.mempoolService.removeAll(entries); + return; + } + + await this.submitTransaction(relayer, transaction, storageMap) + .then(async (txHash: string) => { + this.logger.debug(`Bundle submitted: ${txHash}`); + this.logger.debug( + `User op hashes ${entries.map((entry) => entry.userOpHash)}` + ); + await this.mempoolService.setStatus( + entries, + MempoolEntryStatus.Submitted, + txHash + ); + + await this.waitForTransaction(txHash).catch((err) => + this.logger.error(err, "Relayer: Could not find transaction") + ); + await this.mempoolService.removeAll(entries); + this.reportSubmittedUserops(txHash, bundle); + }) + .catch(async (err: any) => { + // Put all userops back to the mempool + // if some userop failed, it will be deleted inside handleUserOpFail() + await this.mempoolService.setStatus( + entries, + MempoolEntryStatus.New + ); + await this.handleUserOpFail(entries, err); + }); + } else { + await relayer + .sendTransaction(transaction) + .catch((err: any) => this.handleUserOpFail(entries, err)); + await this.mempoolService.removeAll(entries); + } + }); + } + + /** + * signs & sends a transaction + * @param relayer wallet + * @param transaction transaction request + * @param storageMap storage map + * @returns transaction hash + */ + private async submitTransaction( + relayer: Relayer, + transaction: providers.TransactionRequest, + storageMap: StorageMap + ): Promise { + const signedRawTx = await relayer.signTransaction(transaction); + const method = !this.networkConfig.conditionalTransactions + ? "eth_sendRawTransaction" + : "eth_sendRawTransactionConditional"; + const params = !this.networkConfig.conditionalTransactions + ? [signedRawTx] + : [signedRawTx, { knownAccounts: storageMap }]; + + this.logger.debug({ + method, + ...transaction, + params, + }); + + let hash = ""; + if (this.networkConfig.rpcEndpointSubmit) { + this.logger.debug("Sending to a separate rpc"); + const provider = new providers.JsonRpcProvider( + this.networkConfig.rpcEndpointSubmit + ); + hash = await provider.send(method, params); + } else { + hash = await this.provider.send(method, params); + } + + this.logger.debug(`Sent new bundle ${hash}`); + return hash; + } +} diff --git a/packages/executor/src/services/BundlingService/relayers/flashbots.ts b/packages/executor/src/services/BundlingService/relayers/flashbots.ts new file mode 100644 index 00000000..367f6de8 --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/flashbots.ts @@ -0,0 +1,178 @@ +import { providers } from "ethers"; +import { PerChainMetrics } from "monitoring/lib"; +import { Logger, NetworkName } from "types/lib"; +import { IEntryPoint__factory } from "types/lib/executor/contracts"; +import { + FlashbotsBundleProvider, + FlashbotsBundleResolution, +} from "@flashbots/ethers-provider-bundle"; +import { MempoolEntryStatus } from "types/lib/executor"; +import { Config } from "../../../config"; +import { Bundle, NetworkConfig } from "../../../interfaces"; +import { MempoolService } from "../../MempoolService"; +import { ReputationService } from "../../ReputationService"; +import { estimateBundleGasLimit } from "../utils"; +import { Relayer } from "../interfaces"; +import { now } from "../../../utils"; +import { BaseRelayer } from "./base"; + +export class FlashbotsRelayer extends BaseRelayer { + private submitTimeout = 5 * 60 * 1000; // 5 minutes + + constructor( + logger: Logger, + chainId: number, + network: NetworkName, + provider: providers.JsonRpcProvider, + config: Config, + networkConfig: NetworkConfig, + mempoolService: MempoolService, + reputationService: ReputationService, + metrics: PerChainMetrics | null + ) { + super( + logger, + chainId, + network, + provider, + config, + networkConfig, + mempoolService, + reputationService, + metrics + ); + } + + async sendBundle(bundle: Bundle, beneficiary: string): Promise { + const availableIndex = this.getAvailableRelayerIndex(); + if (availableIndex == null) return; + + const relayer = this.relayers[availableIndex]; + const mutex = this.mutexes[availableIndex]; + + const { entries } = bundle; + if (!bundle.entries.length) return; + + await mutex.runExclusive(async (): Promise => { + const entryPoint = entries[0]!.entryPoint; + const entryPointContract = IEntryPoint__factory.connect( + entryPoint, + this.provider + ); + + const txRequest = entryPointContract.interface.encodeFunctionData( + "handleOps", + [entries.map((entry) => entry.userOp), beneficiary] + ); + + const transactionRequest: providers.TransactionRequest = { + to: entryPoint, + data: txRequest, + type: 2, + maxPriorityFeePerGas: bundle.maxPriorityFeePerGas, + maxFeePerGas: bundle.maxFeePerGas, + gasLimit: estimateBundleGasLimit( + this.networkConfig.bundleGasLimitMarkup, + bundle.entries + ), + chainId: this.provider._network.chainId, + nonce: await relayer.getTransactionCount(), + }; + + try { + // checking for tx revert + await relayer.estimateGas(transactionRequest); + } catch (err) { + this.logger.error(err); + await this.mempoolService.removeAll(entries); + return; + } + + await this.submitTransaction(relayer, transactionRequest) + .then(async (txHash) => { + this.logger.debug(`Flashbots: Bundle submitted: ${txHash}`); + this.logger.debug( + `Flashbots: User op hashes ${entries.map( + (entry) => entry.userOpHash + )}` + ); + await this.mempoolService.setStatus( + entries, + MempoolEntryStatus.Submitted, + txHash + ); + await this.waitForTransaction(txHash).catch((err) => + this.logger.error(err, "Flashbots: Could not find transaction") + ); + await this.mempoolService.removeAll(entries); + this.reportSubmittedUserops(txHash, bundle); + }) + .catch(async (err: any) => { + // Put all userops back to the mempool + // if some userop failed, it will be deleted inside handleUserOpFail() + await this.mempoolService.setStatus(entries, MempoolEntryStatus.New); + if (err === "timeout") { + this.logger.debug("Flashbots: Timeout"); + return; + } + await this.handleUserOpFail(entries, err); + return; + }); + }); + } + + /** + * signs & sends a transaction + * @param signer wallet + * @param transaction transaction request + * @param storageMap storage map + * @returns transaction hash + */ + private async submitTransaction( + signer: Relayer, + transaction: providers.TransactionRequest + ): Promise { + this.logger.debug(transaction, "Flashbots: Submitting"); + const fbProvider = await FlashbotsBundleProvider.create( + this.provider, + signer, + this.networkConfig.rpcEndpointSubmit, + this.network + ); + const submitStart = now(); + return new Promise((resolve, reject) => { + let lock = false; + const handler = async (blockNumber: number): Promise => { + if (now() - submitStart > this.submitTimeout) return reject("timeout"); + if (lock) return; + lock = true; + const targetBlock = blockNumber + 1; + const signedBundle = await fbProvider.signBundle([ + { signer, transaction }, + ]); + this.logger.debug( + `Flashbots: Trying to submit to block ${targetBlock}` + ); + const bundleReceipt = await fbProvider.sendRawBundle( + signedBundle, + targetBlock + ); + if ("error" in bundleReceipt) { + this.provider.removeListener("block", handler); + return reject(bundleReceipt.error); + } + const waitResponse = await bundleReceipt.wait(); + lock = false; + if (FlashbotsBundleResolution[waitResponse] === "BundleIncluded") { + this.provider.removeListener("block", handler); + resolve(bundleReceipt.bundleHash); + } + if (FlashbotsBundleResolution[waitResponse] === "AccountNonceTooHigh") { + this.provider.removeListener("block", handler); + return reject("AccountNonceTooHigh"); + } + }; + this.provider.on("block", handler); + }); + } +} diff --git a/packages/executor/src/services/BundlingService/relayers/index.ts b/packages/executor/src/services/BundlingService/relayers/index.ts new file mode 100644 index 00000000..15414380 --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/index.ts @@ -0,0 +1,2 @@ +export * from "./classic"; +export * from "./flashbots"; diff --git a/packages/executor/src/services/BundlingService/service.ts b/packages/executor/src/services/BundlingService/service.ts new file mode 100644 index 00000000..532e384b --- /dev/null +++ b/packages/executor/src/services/BundlingService/service.ts @@ -0,0 +1,401 @@ +import { BigNumber, providers } from "ethers"; +import { PerChainMetrics } from "monitoring/lib"; +import { NetworkName, Logger } from "types/lib"; +import { BundlingMode } from "types/lib/api/interfaces"; +import { IEntryPoint__factory } from "types/lib/executor/contracts"; +import { + MempoolEntryStatus, + RelayingMode, + ReputationStatus, +} from "types/lib/executor"; +import { GasPriceMarkupOne, chainsWithoutEIP1559, getGasFee } from "params/lib"; +import { IGetGasFeeResult } from "params/lib/gas-price-oracles/oracles"; +import { Mutex } from "async-mutex"; +import { Config } from "../../config"; +import { + Bundle, + NetworkConfig, + UserOpValidationResult, +} from "../../interfaces"; +import { MempoolService } from "../MempoolService"; +import { ReputationService } from "../ReputationService"; +import { UserOpValidationService } from "../UserOpValidation"; +import { mergeStorageMap } from "../../utils/mergeStorageMap"; +import { getAddr, wait } from "../../utils"; +import { MempoolEntry } from "../../entities/MempoolEntry"; +import { IRelayingMode } from "./interfaces"; +import { ClassicRelayer, FlashbotsRelayer } from "./relayers"; + +export class BundlingService { + private mutex: Mutex; + private bundlingMode: BundlingMode; + private autoBundlingInterval: number; + private autoBundlingCron?: NodeJS.Timer; + private maxBundleSize: number; + private networkConfig: NetworkConfig; + private relayer: IRelayingMode; + private maxSubmitAttempts = 3; + + constructor( + private chainId: number, + private network: NetworkName, + private provider: providers.JsonRpcProvider, + private mempoolService: MempoolService, + private userOpValidationService: UserOpValidationService, + private reputationService: ReputationService, + private config: Config, + private logger: Logger, + private metrics: PerChainMetrics | null, + relayingMode: RelayingMode + ) { + this.mutex = new Mutex(); + this.networkConfig = config.getNetworkConfig(network)!; + + if (relayingMode === "flashbots") { + this.logger.debug(`${this.network}: Using flashbots relayer`); + this.relayer = new FlashbotsRelayer( + this.logger, + this.chainId, + this.network, + this.provider, + this.config, + this.networkConfig, + this.mempoolService, + this.reputationService, + this.metrics + ); + } else { + this.relayer = new ClassicRelayer( + this.logger, + this.chainId, + this.network, + this.provider, + this.config, + this.networkConfig, + this.mempoolService, + this.reputationService, + this.metrics + ); + } + + this.bundlingMode = "auto"; + this.autoBundlingInterval = this.networkConfig.bundleInterval; + this.maxBundleSize = this.networkConfig.bundleSize; + this.restartCron(); + } + + setBundlingMode(mode: BundlingMode): void { + this.bundlingMode = mode; + this.restartCron(); + } + + setBundlingInverval(interval: number): void { + if (interval > 1) { + this.autoBundlingInterval = interval * 1000; + this.restartCron(); + } + } + + private async createBundle( + gasFee: IGetGasFeeResult, + entries: MempoolEntry[] + ): Promise { + // TODO: support multiple entry points + // filter bundles by entry points + const bundle: Bundle = { + storageMap: {}, + entries: [], + maxFeePerGas: BigNumber.from(0), + maxPriorityFeePerGas: BigNumber.from(0), + }; + + const paymasterDeposit: { [key: string]: BigNumber } = {}; + const stakedEntityCount: { [key: string]: number } = {}; + const senders = new Set(); + const knownSenders = entries.map((it) => { + return it.userOp.sender.toLowerCase(); + }); + + for (const entry of entries) { + // validate gas prices if enabled + if (this.networkConfig.enforceGasPrice) { + let { maxPriorityFeePerGas, maxFeePerGas } = gasFee; + const { enforceGasPriceThreshold } = this.networkConfig; + if (chainsWithoutEIP1559.some((chainId) => chainId === this.chainId)) { + maxFeePerGas = maxPriorityFeePerGas = gasFee.gasPrice; + } + // userop max fee per gas = userop.maxFee * (100 + threshold) / 100; + const userOpMaxFeePerGas = BigNumber.from(entry.userOp.maxFeePerGas) + .mul(GasPriceMarkupOne.add(enforceGasPriceThreshold)) + .div(GasPriceMarkupOne); + // userop priority fee per gas = userop.priorityFee * (100 + threshold) / 100; + const userOpmaxPriorityFeePerGas = BigNumber.from( + entry.userOp.maxPriorityFeePerGas + ) + .mul(GasPriceMarkupOne.add(enforceGasPriceThreshold)) + .div(GasPriceMarkupOne); + if ( + userOpMaxFeePerGas.lt(maxFeePerGas!) || + userOpmaxPriorityFeePerGas.lt(maxPriorityFeePerGas!) + ) { + this.logger.debug( + { + sender: entry.userOp.sender, + nonce: entry.userOp.nonce.toString(), + userOpMaxFeePerGas: userOpMaxFeePerGas.toString(), + userOpmaxPriorityFeePerGas: userOpmaxPriorityFeePerGas.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGas!.toString(), + maxFeePerGas: maxFeePerGas!.toString(), + }, + "Skipping user op with low gas price" + ); + continue; + } + } + + const entities = { + paymaster: getAddr(entry.userOp.paymasterAndData), + factory: getAddr(entry.userOp.initCode), + }; + for (const [title, entity] of Object.entries(entities)) { + if (!entity) continue; + const status = await this.reputationService.getStatus(entity); + if (status === ReputationStatus.BANNED) { + await this.mempoolService.remove(entry); + continue; + } else if ( + status === ReputationStatus.THROTTLED || + (stakedEntityCount[entity] ?? 0) > 1 + ) { + this.logger.debug( + { + sender: entry.userOp.sender, + nonce: entry.userOp.nonce, + entity, + }, + `skipping throttled ${title}` + ); + continue; + } + } + + if (senders.has(entry.userOp.sender)) { + this.logger.debug( + { sender: entry.userOp.sender, nonce: entry.userOp.nonce }, + "skipping already included sender" + ); + continue; + } + + let validationResult: UserOpValidationResult; + try { + validationResult = + await this.userOpValidationService.simulateValidation( + entry.userOp, + entry.entryPoint, + entry.hash + ); + } catch (e: any) { + this.logger.debug(`failed 2nd validation: ${e.message}`); + await this.mempoolService.remove(entry); + continue; + } + + // Check if userOp is trying to access storage of another userop + if (validationResult.storageMap) { + const sender = entry.userOp.sender.toLowerCase(); + const conflictingSender = Object.keys(validationResult.storageMap) + .map((address) => address.toLowerCase()) + .find((address) => { + return address !== sender && knownSenders.includes(address); + }); + if (conflictingSender) { + this.logger.debug( + `UserOperation from ${entry.userOp.sender} sender accessed a storage of another known sender ${conflictingSender}` + ); + continue; + } + } + + // TODO: add total gas cap + const entryPointContract = IEntryPoint__factory.connect( + entry.entryPoint, + this.provider + ); + if (entities.paymaster) { + const { paymaster } = entities; + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!paymasterDeposit[paymaster]) { + paymasterDeposit[paymaster] = await entryPointContract.balanceOf( + paymaster + ); + } + if ( + paymasterDeposit[paymaster]?.lt(validationResult.returnInfo.prefund) + ) { + // not enough balance in paymaster to pay for all UserOps + // (but it passed validation, so it can sponsor them separately + continue; + } + stakedEntityCount[paymaster] = (stakedEntityCount[paymaster] ?? 0) + 1; + paymasterDeposit[paymaster] = BigNumber.from( + paymasterDeposit[paymaster]?.sub(validationResult.returnInfo.prefund) + ); + } + + if (entities.factory) { + const { factory } = entities; + stakedEntityCount[factory] = (stakedEntityCount[factory] ?? 0) + 1; + } + + senders.add(entry.userOp.sender); + + this.metrics?.useropsAttempted.inc(); + + if ( + (this.networkConfig.conditionalTransactions || + this.networkConfig.eip2930) && + validationResult.storageMap + ) { + if (BigNumber.from(entry.userOp.nonce).gt(0)) { + const { storageHash } = await this.provider.send("eth_getProof", [ + entry.userOp.sender, + [], + "latest", + ]); + bundle.storageMap[entry.userOp.sender.toLowerCase()] = storageHash; + } + mergeStorageMap(bundle.storageMap, validationResult.storageMap); + } + bundle.entries.push(entry); + + const { maxFeePerGas, maxPriorityFeePerGas } = bundle; + bundle.maxFeePerGas = maxFeePerGas.add(entry.userOp.maxFeePerGas); + bundle.maxPriorityFeePerGas = maxPriorityFeePerGas.add( + entry.userOp.maxPriorityFeePerGas + ); + } + + // skip gas fee protection on Fuse + if (this.provider.network.chainId == 122) { + bundle.maxFeePerGas = BigNumber.from(gasFee.maxFeePerGas); + bundle.maxPriorityFeePerGas = BigNumber.from(gasFee.maxPriorityFeePerGas); + return bundle; + } + + if (bundle.entries.length > 1) { + // average of userops + bundle.maxFeePerGas = bundle.maxFeePerGas.div(bundle.entries.length); + bundle.maxPriorityFeePerGas = bundle.maxPriorityFeePerGas.div( + bundle.entries.length + ); + } + + // if onchain fee is less than userops fee, use onchain fee + if ( + bundle.maxFeePerGas.gt(gasFee.maxFeePerGas ?? gasFee.gasPrice!) && + bundle.maxPriorityFeePerGas.gt(gasFee.maxPriorityFeePerGas!) + ) { + bundle.maxFeePerGas = BigNumber.from( + gasFee.maxFeePerGas ?? gasFee.gasPrice! + ); + bundle.maxPriorityFeePerGas = BigNumber.from( + gasFee.maxPriorityFeePerGas! + ); + } + + return bundle; + } + + private restartCron(): void { + if (this.autoBundlingCron) { + clearInterval(this.autoBundlingCron); + } + if (this.bundlingMode !== "auto") { + return; + } + this.autoBundlingCron = setInterval(() => { + void this.tryBundle(); + }, this.autoBundlingInterval); + } + + /** + * determine who should receive the proceedings of the request. + * if signer's balance is too low, send it to signer. otherwise, send to configured beneficiary. + */ + private async selectBeneficiary(): Promise { + const config = this.config.getNetworkConfig(this.network); + let beneficiary = this.config.getBeneficiary(this.network); + const signer = this.config.getRelayer(this.network); + const signerAddress = await signer!.getAddress(); + const currentBalance = await this.provider.getBalance(signerAddress); + + if (currentBalance.lte(config!.minSignerBalance) || !beneficiary) { + beneficiary = signerAddress; + this.logger.info( + `low balance on ${signerAddress}. using it as beneficiary` + ); + } + return beneficiary; + } + + async sendNextBundle(): Promise { + await this.mutex.runExclusive(async () => { + let entries = await this.mempoolService.getNewEntriesSorted(); + if (!entries.length) return; + if (this.relayer.isLocked()) { + this.logger.debug("Have userops, but all relayers are busy."); + return; + } + + // remove entries from mempool if submitAttempts are greater than maxAttemps + const invalidEntries = entries.filter( + (entry) => entry.submitAttempts >= this.maxSubmitAttempts + ); + if (invalidEntries.length > 0) { + this.logger.debug( + `Found ${invalidEntries.length} problematic user ops, deleting...` + ); + await this.mempoolService.removeAll(invalidEntries); + entries = await this.mempoolService.getNewEntriesSorted(); + } + if (!entries.length) return; + const gasFee = await getGasFee( + this.chainId, + this.provider, + this.networkConfig.etherscanApiKey + ); + if ( + gasFee.gasPrice == undefined && + gasFee.maxFeePerGas == undefined && + gasFee.maxPriorityFeePerGas == undefined + ) { + this.logger.debug("Could not fetch gas prices..."); + return; + } + const bundle = await this.createBundle(gasFee, entries); + if (!bundle.entries.length) return; + await this.mempoolService.setStatus( + bundle.entries, + MempoolEntryStatus.Pending + ); + await this.mempoolService.attemptToBundle(bundle.entries); + void this.relayer + .sendBundle(bundle, await this.selectBeneficiary()) + .catch((err) => { + this.logger.error(err); + }); + this.logger.debug("Sent new bundle to Skandha relayer..."); + + // during testing against spec-tests we need to wait the block to be submitted + if (this.config.testingMode) { + await wait(500); + } + }); + } + + // assemble and send new bundle + private async tryBundle(): Promise { + await this.sendNextBundle().catch((err) => this.logger.error(err)); + } +} diff --git a/packages/executor/src/services/BundlingService/utils/estimateBundleGasLimit.ts b/packages/executor/src/services/BundlingService/utils/estimateBundleGasLimit.ts new file mode 100644 index 00000000..1f74c99f --- /dev/null +++ b/packages/executor/src/services/BundlingService/utils/estimateBundleGasLimit.ts @@ -0,0 +1,23 @@ +import { BigNumber } from "ethers"; +import { MempoolEntry } from "../../../entities/MempoolEntry"; + +export function estimateBundleGasLimit( + markup: number, + bundle: MempoolEntry[] +): BigNumber { + let gasLimit = BigNumber.from(markup); + for (const { userOp } of bundle) { + gasLimit = BigNumber.from(userOp.verificationGasLimit) + .mul(3) + .add(userOp.preVerificationGas) + .add(userOp.callGasLimit) + .mul(11) + .div(10) + .add(gasLimit); + } + if (gasLimit.lt(1e5)) { + // gasLimit should at least be 1e5 to pass test in test-executor + gasLimit = BigNumber.from(1e5); + } + return gasLimit; +} diff --git a/packages/executor/src/services/BundlingService/utils/getUserOpHashes.ts b/packages/executor/src/services/BundlingService/utils/getUserOpHashes.ts new file mode 100644 index 00000000..dceaf043 --- /dev/null +++ b/packages/executor/src/services/BundlingService/utils/getUserOpHashes.ts @@ -0,0 +1,38 @@ +import { IEntryPoint } from "types/lib/executor/contracts"; +import { providers } from "ethers"; +import { IMulticall3__factory } from "types/lib/executor/contracts/factories/IMulticall3__factory"; +import { MempoolEntry } from "../../../entities/MempoolEntry"; + +/** + * returns userop hashes + * @param entryPoint address of the entrypoint + * @param userOps mempool entries + * @param provider rpc provider + * @param multicall address of the multicall3 contract + */ +export async function getUserOpHashes( + entryPoint: IEntryPoint, + userOps: MempoolEntry[], + provider: providers.JsonRpcProvider, + multicall: string +): Promise { + if (userOps.length === 1) { + return [await entryPoint.callStatic.getUserOpHash(userOps[0].userOp)]; + } + try { + const multicallContract = IMulticall3__factory.connect(multicall, provider); + const callDatas = userOps.map((op) => + entryPoint.interface.encodeFunctionData("getUserOpHash", [op.userOp]) + ); + const result = await multicallContract.callStatic.aggregate3( + callDatas.map((data) => ({ + target: entryPoint.address, + callData: data, + allowFailure: false, + })) + ); + return result.map((call) => call.returnData); + } catch (err) { + return []; + } +} diff --git a/packages/executor/src/services/BundlingService/utils/index.ts b/packages/executor/src/services/BundlingService/utils/index.ts new file mode 100644 index 00000000..9a2ed7fa --- /dev/null +++ b/packages/executor/src/services/BundlingService/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./getUserOpHashes"; +export * from "./estimateBundleGasLimit"; diff --git a/packages/executor/src/services/MempoolService.ts b/packages/executor/src/services/MempoolService.ts index 83ff148f..3582cf4e 100644 --- a/packages/executor/src/services/MempoolService.ts +++ b/packages/executor/src/services/MempoolService.ts @@ -5,6 +5,7 @@ import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint"; import { IEntityWithAggregator, + MempoolEntryStatus, IWhitelistedEntities, ReputationStatus, } from "types/lib/executor"; @@ -90,6 +91,12 @@ export class MempoolService { await this.updateSeenStatus(userOp, aggregator); } + async removeAll(entries: MempoolEntry[]): Promise { + for (const entry of entries) { + await this.remove(entry); + } + } + async remove(entry: MempoolEntry | null): Promise { if (!entry) { return; @@ -100,6 +107,30 @@ export class MempoolService { await this.db.put(this.USEROP_COLLECTION_KEY, newKeys); } + async attemptToBundle(entries: MempoolEntry[]): Promise { + for (const entry of entries) { + entry.submitAttempts++; + await this.db.put(this.getKey(entry), { + ...entry, + lastUpdatedTime: now(), + }); + } + } + + async setStatus( + entries: MempoolEntry[], + status: MempoolEntryStatus, + txHash?: string + ): Promise { + for (const entry of entries) { + entry.setStatus(status, txHash); + await this.db.put(this.getKey(entry), { + ...entry, + lastUpdatedTime: now(), + }); + } + } + async saveUserOpHash(hash: string, entry: MempoolEntry): Promise { const key = this.getKey(entry); await this.db.put(`${this.USEROP_HASHES_COLLECTION_PREFIX}${hash}`, key); @@ -117,9 +148,11 @@ export class MempoolService { return this.findByKey(key); } - async getSortedOps(): Promise { + async getNewEntriesSorted(): Promise { const allEntries = await this.fetchAll(); - return allEntries.sort(MempoolEntry.compareByCost); + return allEntries + .filter((entry) => entry.status === MempoolEntryStatus.New) + .sort(MempoolEntry.compareByCost); } async clearState(): Promise { @@ -333,6 +366,9 @@ export class MempoolService { hash: raw.hash, userOpHash: raw.userOpHash, lastUpdatedTime: raw.lastUpdatedTime, + transaction: raw.transaction, + status: raw.status, + submitAttempts: raw.submitAttempts, }); } diff --git a/packages/executor/src/utils/index.ts b/packages/executor/src/utils/index.ts index 98b4c7a3..44e7a678 100644 --- a/packages/executor/src/utils/index.ts +++ b/packages/executor/src/utils/index.ts @@ -146,10 +146,18 @@ export function extractAddrFromInitCode(data?: BytesLike): string | undefined { return undefined; } +/** + * Unix timestamp * 1000 + * @returns time in milliseconds + */ export function now(): number { return new Date().getTime(); } +export function wait(milliseconds: number): Promise { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} + export function getAddr(data?: BytesLike): string | undefined { if (data == null) { return undefined; diff --git a/packages/monitoring/package.json b/packages/monitoring/package.json index 7a56f24c..13511e3a 100644 --- a/packages/monitoring/package.json +++ b/packages/monitoring/package.json @@ -1,6 +1,6 @@ { "name": "monitoring", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "The Monitoring module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://github.com/etherspot/etherspot-bundler#readme", @@ -32,6 +32,6 @@ }, "dependencies": { "prom-client": "^14.2.0", - "types": "^1.0.26-alpha" + "types": "^1.0.27-alpha" } } diff --git a/packages/node/package.json b/packages/node/package.json index aec91057..6b1d9695 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "node", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "The bundler node module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -56,25 +56,25 @@ "@libp2p/tcp": "6.1.0", "@multiformats/multiaddr": "11.4.0", "abstract-leveldown": "7.2.0", - "api": "^1.0.26-alpha", + "api": "^1.0.27-alpha", "datastore-core": "8.0.1", - "db": "^1.0.26-alpha", + "db": "^1.0.27-alpha", "ethers": "5.7.2", - "executor": "^1.0.26-alpha", + "executor": "^1.0.27-alpha", "it-filter": "1.0.2", "it-map": "1.0.5", "it-sort": "1.0.0", "it-take": "1.0.1", "libp2p": "0.42.2", - "monitoring": "^1.0.26-alpha", - "params": "^1.0.26-alpha", + "monitoring": "^1.0.27-alpha", + "params": "^1.0.27-alpha", "prettier": "2.8.4", "snappy": "7.2.2", "snappyjs": "0.7.0", "stream-to-it": "0.2.4", "strict-event-emitter-types": "2.0.0", - "types": "^1.0.26-alpha", - "utils": "^1.0.26-alpha", + "types": "^1.0.27-alpha", + "utils": "^1.0.27-alpha", "varint": "6.0.0", "xxhash-wasm": "1.0.2" }, diff --git a/packages/params/package.json b/packages/params/package.json index 411bce0b..45639a9a 100644 --- a/packages/params/package.json +++ b/packages/params/package.json @@ -1,6 +1,6 @@ { "name": "params", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "Various bundler parameters", "author": "Etherspot", "homepage": "https://github.com/etherspot/skandha#readme", @@ -26,8 +26,8 @@ "@eth-optimism/sdk": "3.0.0", "@mantleio/sdk": "0.2.1", "ethers": "5.7.2", - "types": "^1.0.26-alpha", - "utils": "^1.0.26-alpha" + "types": "^1.0.27-alpha", + "utils": "^1.0.27-alpha" }, "scripts": { "clean": "rm -rf lib && rm -f *.tsbuildinfo", diff --git a/packages/types/package.json b/packages/types/package.json index fab08243..89c9428d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "types", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "The types of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", diff --git a/packages/types/src/api/interfaces.ts b/packages/types/src/api/interfaces.ts index 29589a69..0c6a6abe 100644 --- a/packages/types/src/api/interfaces.ts +++ b/packages/types/src/api/interfaces.ts @@ -50,7 +50,6 @@ export type GetConfigResponse = { flags: { redirectRpc: boolean; testingMode: boolean; - unsafeMode: boolean; }; entryPoints: string[]; beneficiary: string; @@ -72,6 +71,11 @@ export type GetConfigResponse = { eip2930: boolean; useropsTTL: number; whitelistedEntities: IWhitelistedEntities; + bundleGasLimitMarkup: number; + relayingMode: string; + bundleInterval: number; + bundleSize: number; + minUnstakeDelay: number; }; export type SupportedEntryPoints = string[]; diff --git a/packages/types/src/executor/entities/MempoolEntry.ts b/packages/types/src/executor/entities/MempoolEntry.ts new file mode 100644 index 00000000..258b236b --- /dev/null +++ b/packages/types/src/executor/entities/MempoolEntry.ts @@ -0,0 +1,8 @@ +export enum MempoolEntryStatus { + New = 0, + Pending = 1, + Submitted = 2, + IncludedToChain = 3, + Finalized = 4, + Cancelled = 5, +} diff --git a/packages/types/src/executor/entities/index.ts b/packages/types/src/executor/entities/index.ts new file mode 100644 index 00000000..129dd0d8 --- /dev/null +++ b/packages/types/src/executor/entities/index.ts @@ -0,0 +1 @@ +export * from "./MempoolEntry"; diff --git a/packages/types/src/executor/index.ts b/packages/types/src/executor/index.ts index cc03d36d..89699564 100644 --- a/packages/types/src/executor/index.ts +++ b/packages/types/src/executor/index.ts @@ -1,3 +1,4 @@ +export type RelayingMode = "flashbots" | "classic"; export interface SendBundleReturn { transactionHash: string; userOpHashes: string[]; @@ -11,3 +12,4 @@ export enum ReputationStatus { export * from "./validation"; export * from "./IWhitelistedEntities"; +export * from "./entities"; diff --git a/packages/utils/package.json b/packages/utils/package.json index adb3b5b4..4dbda0b8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "utils", - "version": "1.0.26-alpha", + "version": "1.0.27-alpha", "description": "utils of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -37,6 +37,6 @@ "case": "^1.6.3", "pino": "8.11.0", "pino-pretty": "10.0.0", - "types": "^1.0.26-alpha" + "types": "^1.0.27-alpha" } } diff --git a/yarn.lock b/yarn.lock index a75573e5..2d555cf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1022,6 +1022,11 @@ dependencies: fast-json-stringify "^5.7.0" +"@flashbots/ethers-provider-bundle@0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@flashbots/ethers-provider-bundle/-/ethers-provider-bundle-0.6.2.tgz#b1c9bf74f29f2715075b60bf7db0557c01692001" + integrity sha512-W4Hi47zWggWgLBwhoxH3qaojudAjcbBU+ldEYi5o06UQm/25Hk/AUvCLiN+9nvy1g3xxpF9QBdMniUwjC4cpBw== + "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"