diff --git a/Dockerfile b/Dockerfile index 2beec413..2f2c89a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=${BUILDPLATFORM:-amd64} node:18-alpine as build_src WORKDIR /usr/app -RUN apk update && apk add --no-cache g++ make python3 && rm -rf /var/cache/apk/* +RUN apk update && apk add --no-cache g++ make python3 git py3-setuptools && rm -rf /var/cache/apk/* COPY . . @@ -10,7 +10,7 @@ RUN yarn install --non-interactive --frozen-lockfile && \ FROM node:18-alpine as build_deps WORKDIR /usr/app -RUN apk update && apk add --no-cache g++ make python3 && rm -rf /var/cache/apk/* +RUN apk update && apk add --no-cache g++ make python3 git py3-setuptools && rm -rf /var/cache/apk/* COPY --from=build_src /usr/app . diff --git a/README.md b/README.md index e56cdc4c..4531f5a3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## Important links **[Install Skandha](https://etherspot.fyi/skandha/installation)** -| [Chains supported](https://etherspot.fyi/skandha/chains) +| [Chains supported](https://etherspot.fyi/prime-sdk/chains-supported) | [UserOp Fee history](https://etherspot.fyi/skandha/feehistory) ## ⚙️ How to run (from Source code) @@ -72,8 +72,9 @@ Or follow the steps below: "entryPoints": [ "0x0000000071727De22E5E9d8BAf0edAc6f37da032" ], - "relayer": "0x{RELAYER-PRIVATE-KEY}", - "beneficiary": "0x{BENEFICIARY-ADDRESS}", + "relayers": [ + "0x{RELAYER-PRIVATE-KEY}" + ], "rpcEndpoint": "https://polygon-mumbai.blockpi.network/v1/rpc/public" } ``` @@ -115,7 +116,7 @@ Or follow the steps below: "account": [] }, "bundleGasLimitMarkup": 25000, # optional, 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 + "relayingMode": "classic"; # optional, "flashbots" for Flashbots Builder API, "merkle" for Merkle.io "bundleInterval": 10000, # bundle creation interval "bundleSize": 4, # optional, max size of a bundle, 4 userops by default "pvgMarkup": 0 # optional, adds some gas on top of estimated PVG diff --git a/docs/skandha_subscribe.md b/docs/skandha_subscribe.md new file mode 100644 index 00000000..5c10f5a9 --- /dev/null +++ b/docs/skandha_subscribe.md @@ -0,0 +1,184 @@ +## skandha_subscribe + +Creates a new subscription for desired events. Sends data as soon as it occurs. + +### Event Types + +- pendingUserOps - user ops validated and put in the mempool +- submittedUserOps - user ops that are submitted on chain, reverted or deleted from mempool +- onChainUserOps - user ops successfully submitted on chain + +### Examples: + +### Pending UserOps + +```json +{ + "method": "skandha_subscribe", + "params": [ + "pendingUserOps" + ], + "id": 1, + "jsonrpc": "2.0" +} +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0x106eb9867751ff1bf61bad4a80b8b486" +} +``` + + +#### Event + +```json +{ + "jsonrpc": "2.0", + "method": "skandha_subscription", + "params": { + "subscription": "0x106eb9867751ff1bf61bad4a80b8b486", + "result": { + "userOp": { + "sender": "0xb582979C2136189475326c648732F76677B16B98", + "nonce": "0x5", + "initCode": "0x", + "callData": "0x47e1da2a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000009fd4f6088f2025427ab1e89257a44747081ed590000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xb957", + "verificationGasLimit": "0x9b32", + "maxFeePerGas": "0x171ab3b64", + "maxPriorityFeePerGas": "0x59682f00", + "paymasterAndData": "0x", + "preVerificationGas": "0xae70", + "signature": "0x260dfe374ec4d662fae1ac99384abc50b0490d9a087877580f585e739be368e424576440db1d2fa8950b32207d023126a48749f86c35192d872b04eed22c4f2d1b" + }, + "userOpHash": "0xf8a549671473d0ee532ca235b4629b239823b426b9a898d20c58ca5212a64c9e", + "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "prefund": "0x2e7a15ccb8c44", + "submittedTime": "0x18f2990121c", + "status": "pending" + } + } +} +``` + +--- + +### Submitted, Reverted, Cancelled User Ops + +```json +{ + "method": "skandha_subscribe", + "params": [ + "submittedUserOps" + ], + "id": 1, + "jsonrpc": "2.0" +} +``` + +#### Event + +```json +{ + "jsonrpc": "2.0", + "method": "skandha_subscription", + "params": { + "subscription": "0x80e0632d2300aa2e1bcdb1e84329963f", + "result": { + "userOp": { + "sender": "0xb582979C2136189475326c648732F76677B16B98", + "nonce": "0x5", + "initCode": "0x", + "callData": "0x47e1da2a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000009fd4f6088f2025427ab1e89257a44747081ed590000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xb957", + "verificationGasLimit": "0x9b32", + "maxFeePerGas": "0x171ab3b64", + "maxPriorityFeePerGas": "0x59682f00", + "paymasterAndData": "0x", + "preVerificationGas": "0xae70", + "signature": "0x260dfe374ec4d662fae1ac99384abc50b0490d9a087877580f585e739be368e424576440db1d2fa8950b32207d023126a48749f86c35192d872b04eed22c4f2d1b" + }, + "userOpHash": "0xf8a549671473d0ee532ca235b4629b239823b426b9a898d20c58ca5212a64c9e", + "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "transaction": "0x3612daa69ec6d4804065e107e9055c9ec25c9c801d199886524e884e98179656", + "status": "Submitted" + } + } +} +``` + +### Onchain UserOps + +#### Request + +```json +{ + "method": "skandha_subscribe", + "params": [ + "onChainUserOps" + ], + "id": 1, + "jsonrpc": "2.0" +} +``` + +#### Event + +```json +{ + "jsonrpc": "2.0", + "method": "skandha_subscription", + "params": { + "subscription": "0x2e8cf00cbe014abca180c1b6eae51173", + "result": { + "userOp": { + "sender": "0xb582979C2136189475326c648732F76677B16B98", + "nonce": "0x6", + "initCode": "0x", + "callData": "0x47e1da2a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000009fd4f6088f2025427ab1e89257a44747081ed590000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xb957", + "verificationGasLimit": "0x9b32", + "maxFeePerGas": "0x1420c636e", + "maxPriorityFeePerGas": "0x59682f00", + "paymasterAndData": "0x", + "preVerificationGas": "0xae70", + "signature": "0xbe055319adb23a465cf7439b7d4c2ab6e86383a100459c9c34942bd9a7fd016273a159b9239fca414633b6163353faa648dc3a41857075cde2cdd1813eb92fbc1c" + }, + "userOpHash": "0xefafb37d346ccfaf183f0474015aacefe178707e78d56d95e19de8950c033393", + "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "transaction": "0x8adba5c0463bd2cce16585871190972f49f00ead733b7005f43bf62c93296233", + "status": "onChain" + } + } +} +``` + +### Unsubscribe + +#### Request + +```json +{ + "method": "skandha_unsubscribe", + "params": [ + "0xcf47424b5f492abfaa97ca5d4aed1f1d" + ], + "id": 1, + "jsonrpc": "2.0" +} +``` + +#### Response + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": "ok" +} +``` \ No newline at end of file diff --git a/docs/skandha_userOperationStatus.md b/docs/skandha_userOperationStatus.md new file mode 100644 index 00000000..e0b3421f --- /dev/null +++ b/docs/skandha_userOperationStatus.md @@ -0,0 +1,107 @@ +## skandha_userOperationStatus + +Returns the status of a userop by its hash + +### Example + +#### Request + +```json +{ + "id": 3, + "method": "skandha_userOperationStatus", + "params": [ + "0x63e222d108878e7f7440036bce49aeb83007708f067ec8d01153961e97fe1c53" + ], + "jsonrpc": "2.0" +} +``` + + +#### Response + +```json +{ + "jsonrpc": "2.0", + "method": "skandha_subscription", + "params": { + "subscription": "0x6f2342e1637fc8ad51426fcee800e0f9", + "result": { + "userOp": { + "sender": "0x310788f30062415E1c6f154dB377bf3F39200178", + "nonce": "0x2", + "callData": "0xb61d27f6000000000000000000000000260e35d7dcddaa7b558d0ff322f5ddd1109f5dab00000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xff339", + "verificationGasLimit": "0x9bc9", + "preVerificationGas": "0xa384", + "maxFeePerGas": "0x28030e335c", + "maxPriorityFeePerGas": "0x1dcd6500", + "signature": "0x364e6ff383f1276f08badae090bafc1b06de94695fb2dbbf8d81c930ea5f35dd688d85ab7695190c882c8981069804b6236daff55e256bef64e2ceaabf4a56441c" + }, + "userOpHash": "0x3e52209fe0323d5d70039327ccf558fad91893b42235045d1707a63b7eebcfad", + "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032", + "prefund": "0x2b0197bc892da28", + "submittedTime": "0x18fe7d72dfc", + "status": "pending" + } + } +} +``` + +#### Response (after some time) + +```json +{ + "jsonrpc": "2.0", + "method": "skandha_subscription", + "params": { + "subscription": "0x2d8a9d5599e0704aad0a024cf1f284ed", + "result": { + "userOp": { + "sender": "0x310788f30062415E1c6f154dB377bf3F39200178", + "nonce": "0x2", + "callData": "0xb61d27f6000000000000000000000000260e35d7dcddaa7b558d0ff322f5ddd1109f5dab00000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xff339", + "verificationGasLimit": "0x9bc9", + "preVerificationGas": "0xa384", + "maxFeePerGas": "0x28030e335c", + "maxPriorityFeePerGas": "0x1dcd6500", + "signature": "0x364e6ff383f1276f08badae090bafc1b06de94695fb2dbbf8d81c930ea5f35dd688d85ab7695190c882c8981069804b6236daff55e256bef64e2ceaabf4a56441c" + }, + "userOpHash": "0x3e52209fe0323d5d70039327ccf558fad91893b42235045d1707a63b7eebcfad", + "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032", + "transaction": "0xf27d85b4e29a33a66eb91d692938cb94a2c35bd1db736e659afa660ad6d40997", + "status": "Submitted" + } + } +} +``` + +#### Response (after appearing on chain) + +```json +{ + "jsonrpc": "2.0", + "method": "skandha_subscription", + "params": { + "subscription": "0xcf69f6e84a1568824afedf7a61c49fe1", + "result": { + "userOp": { + "sender": "0x310788f30062415E1c6f154dB377bf3F39200178", + "nonce": "0x2", + "callData": "0xb61d27f6000000000000000000000000260e35d7dcddaa7b558d0ff322f5ddd1109f5dab00000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xff339", + "verificationGasLimit": "0x9bc9", + "preVerificationGas": "0xa384", + "maxFeePerGas": "0x28030e335c", + "maxPriorityFeePerGas": "0x1dcd6500", + "signature": "0x364e6ff383f1276f08badae090bafc1b06de94695fb2dbbf8d81c930ea5f35dd688d85ab7695190c882c8981069804b6236daff55e256bef64e2ceaabf4a56441c" + }, + "userOpHash": "0x3e52209fe0323d5d70039327ccf558fad91893b42235045d1707a63b7eebcfad", + "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032", + "transaction": "0xf27d85b4e29a33a66eb91d692938cb94a2c35bd1db736e659afa660ad6d40997", + "status": "onChain" + } + } +} +``` \ No newline at end of file diff --git a/lerna.json b/lerna.json index ba2fd73c..63173889 100644 --- a/lerna.json +++ b/lerna.json @@ -3,7 +3,7 @@ "packages/*" ], "npmClient": "yarn", - "version": "2.0.1", + "version": "2.0.2", "stream": "true", "command": { "version": { diff --git a/package.json b/package.json index 35f2bd94..180aac93 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "root", "private": true, - "version": "2.0.1", + "version": "2.0.2", "engines": { "node": ">=18.0.0" }, @@ -33,8 +33,8 @@ "@types/abstract-leveldown": "7.2.1", "@types/compression": "1.7.2", "@types/node": "18.11.9", - "@typescript-eslint/eslint-plugin": "5.43.0", - "@typescript-eslint/parser": "5.43.0", + "@typescript-eslint/parser": "6.21.0", + "@typescript-eslint/eslint-plugin": "6.21.0", "eslint": "8.27.0", "eslint-config-airbnb-base": "15.0.0", "eslint-plugin-import": "2.26.0", @@ -43,7 +43,7 @@ "lerna": "7.3.0", "ts-node": "10.9.1", "tsconfig-paths": "4.1.2", - "typescript": "4.8.4", + "typescript": "5.4.5", "chai": "4.3.8", "chai-as-promised": "7.1.1", "sinon": "16.0.0", diff --git a/packages/api/package.json b/packages/api/package.json index b82bc3ea..fdd69211 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "description": "The API module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -32,19 +32,22 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@fastify/cors": "8.2.1", - "@skandha/executor": "^2.0.1", - "@skandha/monitoring": "^2.0.1", - "@skandha/types": "^2.0.1", + "@fastify/cors": "9.0.1", + "@fastify/websocket": "10.0.1", + "@skandha/executor": "^2.0.2", + "@skandha/monitoring": "^2.0.2", + "@skandha/types": "^2.0.2", + "@skandha/utils": "^2.0.2", "class-transformer": "0.5.1", "class-validator": "0.14.1", "ethers": "5.7.2", - "fastify": "4.14.1", + "fastify": "4.25.2", "pino": "8.11.0", "pino-pretty": "10.0.0", - "reflect-metadata": "0.1.13" + "reflect-metadata": "0.1.13", + "ws": "8.16.0" }, "devDependencies": { - "@types/connect": "3.4.35" + "@types/ws": "8.2.2" } } diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index a680f50c..673593ca 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -1,24 +1,32 @@ +import { WebSocket } from "ws"; import { Executor } from "@skandha/executor/lib/executor"; import { Config } from "@skandha/executor/lib/config"; import RpcError from "@skandha/types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; -import { FastifyInstance, RouteHandler } from "fastify"; +import { deepHexlify } from "@skandha/utils/lib/hexlify"; import { BundlerRPCMethods, CustomRPCMethods, HttpStatus, RedirectedRPCMethods, } from "./constants"; -import { EthAPI, DebugAPI, Web3API, RedirectAPI } from "./modules"; -import { deepHexlify } from "./utils"; +import { + EthAPI, + DebugAPI, + Web3API, + RedirectAPI, + SubscriptionApi, +} from "./modules"; import { SkandhaAPI } from "./modules/skandha"; +import { JsonRpcRequest, JsonRpcResponse } from "./interface"; +import { Server } from "./server"; export interface RpcHandlerOptions { config: Config; } export interface EtherspotBundlerOptions { - server: FastifyInstance; + server: Server; config: Config; executor: Executor; testingMode: boolean; @@ -34,171 +42,277 @@ export interface RelayerAPI { } export class ApiApp { - private server: FastifyInstance; + private server: Server; private config: Config; private executor: Executor; private testingMode = false; private redirectRpc = false; + private ethApi: EthAPI; + private debugApi: DebugAPI; + private web3Api: Web3API; + private redirectApi: RedirectAPI; + private skandhaApi: SkandhaAPI; + private subscriptionApi: SubscriptionApi; + constructor(options: EtherspotBundlerOptions) { this.server = options.server; this.config = options.config; this.testingMode = options.testingMode; this.redirectRpc = options.redirectRpc; this.executor = options.executor; - this.server.post("/rpc/", this.setupRoutes()); - } - private setupRoutes(): RouteHandler { - const { executor } = this; - const ethApi = new EthAPI(executor.eth); - const debugApi = new DebugAPI(executor.debug); - const web3Api = new Web3API(executor.web3); - const redirectApi = new RedirectAPI(this.config); - const skandhaApi = new SkandhaAPI(executor.eth, executor.skandha); - - const handleRpc = async (ip: string, request: any): Promise => { - let result: any; - const { method, params, jsonrpc, id } = request; - // ADMIN METHODS - if (this.testingMode || ip === "localhost" || ip === "127.0.0.1") { - switch (method) { - case BundlerRPCMethods.debug_bundler_setBundlingMode: - result = await debugApi.setBundlingMode(params[0]); - break; - case BundlerRPCMethods.debug_bundler_setBundleInterval: - result = await debugApi.setBundlingInterval({ - interval: params[0], - }); - break; - case BundlerRPCMethods.debug_bundler_clearState: - result = await debugApi.clearState(); - break; - case BundlerRPCMethods.debug_bundler_clearMempool: - result = await debugApi.clearMempool(); - break; - case BundlerRPCMethods.debug_bundler_dumpMempool: - result = await debugApi.dumpMempool(/* params[0] */); - break; - case BundlerRPCMethods.debug_bundler_setReputation: - result = await debugApi.setReputation({ - reputations: params[0], - entryPoint: params[1], - }); - break; - case BundlerRPCMethods.debug_bundler_dumpReputation: - result = await debugApi.dumpReputation({ - entryPoint: params[0], - }); - break; - case BundlerRPCMethods.debug_bundler_sendBundleNow: - result = await debugApi.sendBundleNow(); - break; - - case BundlerRPCMethods.debug_bundler_setMempool: - result = await debugApi.setMempool({ - userOps: params[0], - entryPoint: params[1], - }); - break; - case BundlerRPCMethods.debug_bundler_getStakeStatus: - result = await debugApi.getStakeStatus({ - address: params[0], - entryPoint: params[1], - }); - break; + this.subscriptionApi = new SubscriptionApi( + this.executor.subscriptionService + ); + this.ethApi = new EthAPI(this.executor.eth); + this.debugApi = new DebugAPI(this.executor.debug); + this.web3Api = new Web3API(this.executor.web3); + this.redirectApi = new RedirectAPI(this.config); + this.skandhaApi = new SkandhaAPI(this.executor.eth, this.executor.skandha); + + // HTTP interface + this.server.http.post("/rpc/", async (req, res): Promise => { + let response = null; + if (Array.isArray(req.body)) { + response = []; + for (const request of req.body) { + response.push( + await this.handleRpcRequest( + request, + req.ip, + req.headers.authorization + ) + ); } + } else { + response = await this.handleRpcRequest( + req.body as JsonRpcRequest, + req.ip, + req.headers.authorization + ); } + return res.status(HttpStatus.OK).send(response); + }); + this.server.http.get("*", async (req, res) => { + void res + .status(200) + .send("GET requests are not supported. Visit https://skandha.fyi"); + }); - if (this.redirectRpc && method in RedirectedRPCMethods) { - const body = await redirectApi.redirect(method, params); - if (body.error) { - return { ...body, id }; + if (this.server.ws != null) { + this.server.ws.get("/rpc/", { websocket: true }, async (socket, _) => { + socket.on("message", async (message) => { + let response: Partial = {}; + try { + const request: JsonRpcRequest = JSON.parse(message.toString()); + const wsRpc = await this.handleWsRequest( + socket, + request as JsonRpcRequest + ); + if (!wsRpc) { + try { + response = await this.handleRpcRequest(request, ""); + } catch (err) { + const { jsonrpc, id } = request; + if (err instanceof RpcError) { + response = { + jsonrpc, + id, + message: err.message, + data: err.data, + }; + } else if (err instanceof Error) { + response = { jsonrpc, id, error: err.message }; + } else { + response = { jsonrpc, id, error: "Internal server error" }; + } + } + } + } catch (err) { + response = { error: "Invalid Request" }; + } + socket.send(JSON.stringify(response)); + }); + }); + } + } + + private async handleWsRequest( + socket: WebSocket, + request: JsonRpcRequest + ): Promise { + let response: JsonRpcResponse | undefined; + const { method, params, jsonrpc, id } = request; + try { + switch (method) { + case CustomRPCMethods.skandha_subscribe: { + const eventId = this.subscriptionApi.subscribe(socket, params[0]); + response = { jsonrpc, id, result: eventId }; + break; + } + case CustomRPCMethods.skandha_unsubscribe: { + this.subscriptionApi.unsubscribe(socket, params[0]); + response = { jsonrpc, id, result: "ok" }; + break; } - return { jsonrpc, id, ...body }; + default: { + return false; // the request can not be handled by this function + } + } + } catch (err) { + if (err instanceof RpcError) { + response = { jsonrpc, id, message: err.message, data: err.data }; + } else if (err instanceof Error) { + response = { jsonrpc, id, error: err.message }; + } else { + response = { jsonrpc, id, error: "Internal server error" }; } + } + if (response != undefined) { + socket.send(JSON.stringify(response)); + } + return true; // the request can not be handled by this function + } + + private async handleRpcRequest( + request: JsonRpcRequest, + ip: string, + authKey?: string + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let result: any; + const { method, params, jsonrpc, id } = request; + if ( + this.testingMode || + ip === "localhost" || + ip === "127.0.0.1" || + (process.env.SKANDHA_ADMIN_KEY && + authKey === process.env.SKANDHA_ADMIN_KEY) + ) { + switch (method) { + case BundlerRPCMethods.debug_bundler_setBundlingMode: + result = await this.debugApi.setBundlingMode(params[0]); + break; + case BundlerRPCMethods.debug_bundler_setBundleInterval: + result = await this.debugApi.setBundlingInterval({ + interval: params[0], + }); + break; + case BundlerRPCMethods.debug_bundler_clearState: + result = await this.debugApi.clearState(); + break; + case BundlerRPCMethods.debug_bundler_clearMempool: + result = await this.debugApi.clearMempool(); + break; + case BundlerRPCMethods.debug_bundler_dumpMempool: + result = await this.debugApi.dumpMempool(/* params[0] */); + break; + case BundlerRPCMethods.debug_bundler_dumpMempoolRaw: + result = await this.debugApi.dumpMempoolRaw(/* params[0] */); + break; + case BundlerRPCMethods.debug_bundler_setReputation: + result = await this.debugApi.setReputation({ + reputations: params[0], + entryPoint: params[1], + }); + break; + case BundlerRPCMethods.debug_bundler_dumpReputation: + result = await this.debugApi.dumpReputation({ + entryPoint: params[0], + }); + break; + case BundlerRPCMethods.debug_bundler_sendBundleNow: + result = await this.debugApi.sendBundleNow(); + break; - if (!result) { - switch (method) { - case BundlerRPCMethods.eth_supportedEntryPoints: - result = await ethApi.getSupportedEntryPoints(); - break; - case BundlerRPCMethods.eth_chainId: - result = await ethApi.getChainId(); - break; - case BundlerRPCMethods.eth_sendUserOperation: - result = await ethApi.sendUserOperation({ + case BundlerRPCMethods.debug_bundler_setMempool: + result = await this.debugApi.setMempool({ + userOps: params[0], + entryPoint: params[1], + }); + break; + case BundlerRPCMethods.debug_bundler_getStakeStatus: + result = await this.debugApi.getStakeStatus({ + address: params[0], + entryPoint: params[1], + }); + break; + } + } + + if (this.redirectRpc && method in RedirectedRPCMethods) { + const body = await this.redirectApi.redirect(method, params); + if (body.error) { + return { ...body, id }; + } + return { jsonrpc, id, ...body }; + } + + if (!result) { + switch (method) { + case BundlerRPCMethods.eth_supportedEntryPoints: + result = await this.ethApi.getSupportedEntryPoints(); + break; + case BundlerRPCMethods.eth_chainId: + result = await this.ethApi.getChainId(); + break; + case BundlerRPCMethods.eth_sendUserOperation: + result = await this.ethApi.sendUserOperation({ + userOp: params[0], + entryPoint: params[1], + }); + break; + case BundlerRPCMethods.eth_estimateUserOperationGas: { + if (this.testingMode) { + result = await this.ethApi.estimateUserOpGasAndValidateSignature({ userOp: params[0], entryPoint: params[1], }); - break; - case BundlerRPCMethods.eth_estimateUserOperationGas: { - if (this.testingMode) { - result = await ethApi.estimateUserOpGasAndValidateSignature({ - userOp: params[0], - entryPoint: params[1], - }); - } else { - result = await ethApi.estimateUserOperationGas({ - userOp: params[0], - entryPoint: params[1], - }); - } - break; - } - case BundlerRPCMethods.eth_getUserOperationReceipt: - result = await ethApi.getUserOperationReceipt(params[0]); - break; - case BundlerRPCMethods.eth_getUserOperationByHash: - result = await ethApi.getUserOperationByHash(params[0]); - break; - case BundlerRPCMethods.web3_clientVersion: - result = web3Api.clientVersion(); - break; - case CustomRPCMethods.skandha_validateUserOperation: - result = await skandhaApi.validateUserOp({ + } else { + result = await this.ethApi.estimateUserOperationGas({ userOp: params[0], entryPoint: params[1], }); - break; - case CustomRPCMethods.skandha_getGasPrice: - result = await skandhaApi.getGasPrice(); - break; - case CustomRPCMethods.skandha_feeHistory: - result = await skandhaApi.getFeeHistory({ - entryPoint: params[0], - blockCount: params[1], - newestBlock: params[2], - }); - break; - case CustomRPCMethods.skandha_config: - result = await skandhaApi.getConfig(); - // skip hexlify for this particular rpc - return { jsonrpc, id, result }; - default: - throw new RpcError( - `Method ${method} is not supported`, - RpcErrorCodes.METHOD_NOT_FOUND - ); + } + break; } + case BundlerRPCMethods.eth_getUserOperationReceipt: + result = await this.ethApi.getUserOperationReceipt(params[0]); + break; + case BundlerRPCMethods.eth_getUserOperationByHash: + result = await this.ethApi.getUserOperationByHash(params[0]); + break; + case BundlerRPCMethods.web3_clientVersion: + result = this.web3Api.clientVersion(); + break; + case CustomRPCMethods.skandha_getGasPrice: + result = await this.skandhaApi.getGasPrice(); + break; + case CustomRPCMethods.skandha_feeHistory: + result = await this.skandhaApi.getFeeHistory({ + entryPoint: params[0], + blockCount: params[1], + newestBlock: params[2], + }); + break; + case CustomRPCMethods.skandha_config: + result = await this.skandhaApi.getConfig(); + // skip hexlify for this particular rpc + return { jsonrpc, id, result }; + case CustomRPCMethods.skandha_userOperationStatus: + result = await this.skandhaApi.getUserOperationStatus(params[0]); + break; + default: + throw new RpcError( + `Method ${method} is not supported`, + RpcErrorCodes.METHOD_NOT_FOUND + ); } + } - result = deepHexlify(result); - return { jsonrpc, id, result }; - }; - - return async (req, res): Promise => { - let response: any = null; - if (Array.isArray(req.body)) { - response = []; - for (const request of req.body) { - response.push(await handleRpc(req.ip, request)); - } - } else { - response = await handleRpc(req.ip, req.body); - } - return res.status(HttpStatus.OK).send(response); - }; + result = deepHexlify(result); + return { jsonrpc, id, result }; } } diff --git a/packages/api/src/constants.ts b/packages/api/src/constants.ts index b19674e6..b092132b 100644 --- a/packages/api/src/constants.ts +++ b/packages/api/src/constants.ts @@ -1,8 +1,10 @@ export const CustomRPCMethods = { - skandha_validateUserOperation: "skandha_validateUserOperation", skandha_getGasPrice: "skandha_getGasPrice", skandha_config: "skandha_config", skandha_feeHistory: "skandha_feeHistory", + skandha_userOperationStatus: "skandha_userOperationStatus", + skandha_subscribe: "skandha_subscribe", + skandha_unsubscribe: "skandha_unsubscribe", }; export const BundlerRPCMethods = { @@ -15,6 +17,7 @@ export const BundlerRPCMethods = { web3_clientVersion: "web3_clientVersion", debug_bundler_clearState: "debug_bundler_clearState", debug_bundler_dumpMempool: "debug_bundler_dumpMempool", + debug_bundler_dumpMempoolRaw: "debug_bundler_dumpMempoolRaw", debug_bundler_setReputation: "debug_bundler_setReputation", debug_bundler_dumpReputation: "debug_bundler_dumpReputation", debug_bundler_setBundlingMode: "debug_bundler_setBundlingMode", diff --git a/packages/api/src/dto/EstimateUserOperation.dto.ts b/packages/api/src/dto/EstimateUserOperation.dto.ts index f01b4e78..9e6ab523 100644 --- a/packages/api/src/dto/EstimateUserOperation.dto.ts +++ b/packages/api/src/dto/EstimateUserOperation.dto.ts @@ -8,7 +8,7 @@ import { } from "class-validator"; import { BigNumberish, BytesLike } from "ethers"; import { Type } from "class-transformer"; -import { IsBigNumber } from "../utils/is-bignumber"; +import { IsBigNumber } from "../utils"; export class EstimateUserOperation { /** @@ -16,22 +16,38 @@ export class EstimateUserOperation { */ @IsEthereumAddress() sender!: string; + @IsBigNumber() nonce!: BigNumberish; + + @IsString() + callData!: BytesLike; + + @IsString() + signature!: BytesLike; + + /** + * Optional properties + */ @IsBigNumber() + @IsOptional() callGasLimit?: BigNumberish; + @IsBigNumber() + @IsOptional() verificationGasLimit?: BigNumberish; + @IsBigNumber() + @IsOptional() preVerificationGas?: BigNumberish; + @IsBigNumber() + @IsOptional() maxFeePerGas?: BigNumberish; + @IsBigNumber() + @IsOptional() maxPriorityFeePerGas?: BigNumberish; - @IsString() - callData!: BytesLike; - @IsString() - signature!: BytesLike; /** * EntryPoint v7 Properties diff --git a/packages/api/src/dto/FeeHistory.dto.ts b/packages/api/src/dto/FeeHistory.dto.ts index 606f75ea..3b1125fb 100644 --- a/packages/api/src/dto/FeeHistory.dto.ts +++ b/packages/api/src/dto/FeeHistory.dto.ts @@ -1,6 +1,6 @@ import { IsEthereumAddress, ValidateIf } from "class-validator"; import { BigNumberish } from "ethers"; -import { IsBigNumber } from "../utils/is-bignumber"; +import { IsBigNumber } from "../utils"; export class FeeHistoryArgs { @IsEthereumAddress() diff --git a/packages/api/src/dto/SendUserOperation.dto.ts b/packages/api/src/dto/SendUserOperation.dto.ts index 826a8f23..bd9e2313 100644 --- a/packages/api/src/dto/SendUserOperation.dto.ts +++ b/packages/api/src/dto/SendUserOperation.dto.ts @@ -8,7 +8,7 @@ import { } from "class-validator"; import { BigNumberish, BytesLike } from "ethers"; import { Type } from "class-transformer"; -import { IsBigNumber } from "../utils/is-bignumber"; +import { IsBigNumber } from "../utils/isBigNumber"; export class SendUserOperation { /** diff --git a/packages/api/src/interface.ts b/packages/api/src/interface.ts new file mode 100644 index 00000000..e4baa0e1 --- /dev/null +++ b/packages/api/src/interface.ts @@ -0,0 +1,15 @@ +export type JsonRpcRequest = { + method: string; + jsonrpc: string; + id: number; + params?: any; +}; + +export type JsonRpcResponse = { + jsonrpc: string; + id: number; + result?: any; + error?: any; + message?: any; + data?: any; +}; diff --git a/packages/api/src/modules/debug.ts b/packages/api/src/modules/debug.ts index 0751d21b..b6badf2a 100644 --- a/packages/api/src/modules/debug.ts +++ b/packages/api/src/modules/debug.ts @@ -3,6 +3,7 @@ import { Debug } from "@skandha/executor/lib/modules"; import { IsEthereumAddress } from "class-validator"; import { BundlingMode } from "@skandha/types/lib/api/interfaces"; import { GetStakeStatus } from "@skandha/executor/lib/interfaces"; +import { MempoolEntrySerialized } from "@skandha/executor/lib/entities/interfaces"; import { RpcMethodValidator } from "../utils/RpcMethodValidator"; import { SetReputationArgs, @@ -53,6 +54,10 @@ export class DebugAPI { return await this.debugModule.dumpMempool(); } + async dumpMempoolRaw(): Promise { + return await this.debugModule.dumpMempoolRaw(); + } + /** * Forces the bundler to build and execute a bundle from the mempool as handleOps() transaction */ diff --git a/packages/api/src/modules/index.ts b/packages/api/src/modules/index.ts index c1426aa7..d0066448 100644 --- a/packages/api/src/modules/index.ts +++ b/packages/api/src/modules/index.ts @@ -2,3 +2,4 @@ export * from "./debug"; export * from "./web3"; export * from "./eth"; export * from "./redirect"; +export * from "./subscription"; diff --git a/packages/api/src/modules/skandha.ts b/packages/api/src/modules/skandha.ts index 79051887..83ab673e 100644 --- a/packages/api/src/modules/skandha.ts +++ b/packages/api/src/modules/skandha.ts @@ -3,27 +3,23 @@ import { GetConfigResponse, GetFeeHistoryResponse, GetGasPriceResponse, + UserOperationStatus, } from "@skandha/types/lib/api/interfaces"; import { Skandha } from "@skandha/executor/lib/modules"; import RpcError from "@skandha/types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; import { RpcMethodValidator } from "../utils/RpcMethodValidator"; -import { SendUserOperationGasArgs } from "../dto/SendUserOperation.dto"; import { FeeHistoryArgs } from "../dto/FeeHistory.dto"; export class SkandhaAPI { constructor(private ethModule: Eth, private skandhaModule: Skandha) {} /** - * Validates UserOp. If the UserOp (sender + entryPoint + nonce) match the existing UserOp in mempool, - * validates if new UserOp can replace the old one (gas fees must be higher by at least 10%) - * @param userOp same as eth_sendUserOperation - * @param entryPoint Entry Point - * @returns + * @params hash hash of a userop + * @returns status */ - @RpcMethodValidator(SendUserOperationGasArgs) - async validateUserOp(args: SendUserOperationGasArgs): Promise { - return await this.ethModule.validateUserOp(args); + async getUserOperationStatus(hash: string): Promise { + return this.skandhaModule.getUserOperationStatus(hash); } /** diff --git a/packages/api/src/modules/subscription.ts b/packages/api/src/modules/subscription.ts new file mode 100644 index 00000000..086ad236 --- /dev/null +++ b/packages/api/src/modules/subscription.ts @@ -0,0 +1,38 @@ +import { WebSocket } from "ws"; +import { + SubscriptionService, + ExecutorEvent, +} from "@skandha/executor/lib/services"; +import RpcError from "@skandha/types/lib/api/errors/rpc-error"; +import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; + +export class SubscriptionApi { + constructor(private subscriptionService: SubscriptionService) {} + + subscribe(socket: WebSocket, event: ExecutorEvent): string { + switch (event) { + case ExecutorEvent.pendingUserOps: { + return this.subscriptionService.listenPendingUserOps(socket); + } + case ExecutorEvent.submittedUserOps: { + return this.subscriptionService.listenSubmittedUserOps(socket); + } + case ExecutorEvent.onChainUserOps: { + return this.subscriptionService.listenOnChainUserOps(socket); + } + case ExecutorEvent.ping: { + return this.subscriptionService.listenPing(socket); + } + default: { + throw new RpcError( + `Event ${event} not supported`, + RpcErrorCodes.METHOD_NOT_FOUND + ); + } + } + } + + unsubscribe(socket: WebSocket, id: string): void { + this.subscriptionService.unsubscribe(socket, id); + } +} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index e06e767e..7fb26b8f 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -1,16 +1,26 @@ -import fastify, { FastifyInstance } from "fastify"; +import fastify, { + FastifyError, + FastifyInstance, + FastifyReply, + FastifyRequest, +} from "fastify"; import cors from "@fastify/cors"; +import websocket from "@fastify/websocket"; import RpcError from "@skandha/types/lib/api/errors/rpc-error"; import { ServerConfig } from "@skandha/types/lib/api/interfaces"; import logger from "./logger"; import { HttpStatus } from "./constants"; +import { JsonRpcRequest } from "./interface"; export class Server { - constructor(private app: FastifyInstance, private config: ServerConfig) { - this.setup(); - } + constructor( + public http: FastifyInstanceAny, + public ws: FastifyInstanceAny | null, + private config: ServerConfig + ) {} static async init(config: ServerConfig): Promise { + let ws: FastifyInstanceAny | null = null; const app = fastify({ logger, disableRequestLogging: !config.enableRequestLogging, @@ -21,6 +31,19 @@ export class Server { origin: config.cors, }); + if (config.ws) { + if (config.wsPort == config.port) { + await app.register(websocket); + ws = app; + } else { + ws = fastify({ + logger, + disableRequestLogging: !config.enableRequestLogging, + ignoreTrailingSlash: true, + }); + } + } + app.addHook("preHandler", (req, reply, done) => { if (req.method === "POST") { req.log.info( @@ -50,22 +73,20 @@ export class Server { done(); }); - return new Server(app, config); - } - - setup(): void { - this.app.get("*", { logLevel: "silent" }, () => { - return "GET requests are not supported. Visit https://skandha.fyi"; - }); + return new Server(app, ws, config); } async listen(): Promise { - this.app.setErrorHandler((err, req, res) => { + const errorHandler = ( + err: FastifyError, + req: FastifyRequest, + res: FastifyReply + ): FastifyReply => { // eslint-disable-next-line no-console logger.error(err); if (err instanceof RpcError) { - const body = req.body as any; + const body = req.body as JsonRpcRequest; const error = { message: err.message, data: err.data, @@ -83,15 +104,43 @@ export class Server { .send({ error: "Unexpected behaviour", }); - }); + }; - await this.app.listen({ - port: this.config.port, - host: this.config.host, - }); - } + this.http.setErrorHandler(errorHandler); + this.http.listen( + { + port: this.config.port, + host: this.config.host, + listenTextResolver: (address) => + `HTTP server listening at ${address}/rpc`, + }, + (err) => { + if (err) throw err; + if (this.http.websocketServer != null) { + this.http.log.info( + `Websocket server listening at ws://${this.config.host}:${this.config.port}/rpc` + ); + } + } + ); - get application(): FastifyInstance { - return this.app; + if (this.config.ws && this.config.wsPort != this.config.port) { + this.ws?.setErrorHandler(errorHandler); + this.ws?.listen( + { + port: this.config.wsPort, + host: this.config.host, + listenTextResolver: () => + `Websocket server listening at ws://${this.config.host}:${this.config.wsPort}/rpc`, + }, + (err) => { + if (err) throw err; + } + ); + } } } + +/// @note to address the bug in fastify types, will be removed in future +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FastifyInstanceAny = FastifyInstance; diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 30e36676..ba4aaeea 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -1,29 +1,3 @@ -import { hexlify } from "ethers/lib/utils"; - export * from "./RpcMethodValidator"; - -/** - * hexlify all members of object, recursively - * @param obj - */ -export function deepHexlify(obj: any): any { - if (typeof obj === "function") { - return undefined; - } - if (obj == null || typeof obj === "string" || typeof obj === "boolean") { - return obj; - // eslint-disable-next-line no-underscore-dangle - } else if (obj._isBigNumber != null || typeof obj !== "object") { - return hexlify(obj).replace(/^0x0/, "0x"); - } - if (Array.isArray(obj)) { - return obj.map((member) => deepHexlify(member)); - } - return Object.keys(obj).reduce( - (set, key) => ({ - ...set, - [key]: deepHexlify(obj[key]), - }), - {} - ); -} +export * from "./isBigNumber"; +export * from "./IsCallCode"; diff --git a/packages/api/src/utils/is-bignumber.ts b/packages/api/src/utils/isBigNumber.ts similarity index 86% rename from packages/api/src/utils/is-bignumber.ts rename to packages/api/src/utils/isBigNumber.ts index 124166b2..0e54f1d1 100644 --- a/packages/api/src/utils/is-bignumber.ts +++ b/packages/api/src/utils/isBigNumber.ts @@ -2,7 +2,7 @@ import { registerDecorator, ValidationOptions } from "class-validator"; import { BigNumber } from "ethers"; export function IsBigNumber(options: ValidationOptions = {}) { - return (object: any, propertyName: string) => { + return (object: object, propertyName: string) => { registerDecorator({ propertyName, options: { @@ -13,7 +13,7 @@ export function IsBigNumber(options: ValidationOptions = {}) { target: object.constructor, constraints: [], validator: { - validate(value: any): boolean { + validate(value: object): boolean { try { return BigNumber.isBigNumber(BigNumber.from(value)); } catch (_) { diff --git a/packages/cli/package.json b/packages/cli/package.json index a5a4067c..327eaee7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "description": "> TODO: description", "author": "zincoshine ", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -40,12 +40,12 @@ "@libp2p/peer-id-factory": "2.0.1", "@libp2p/prometheus-metrics": "1.1.3", "@multiformats/multiaddr": "12.1.3", - "@skandha/api": "^2.0.1", - "@skandha/db": "^2.0.1", - "@skandha/executor": "^2.0.1", - "@skandha/monitoring": "^2.0.1", - "@skandha/node": "^2.0.1", - "@skandha/types": "^2.0.1", + "@skandha/api": "^2.0.2", + "@skandha/db": "^2.0.2", + "@skandha/executor": "^2.0.2", + "@skandha/monitoring": "^2.0.2", + "@skandha/node": "^2.0.2", + "@skandha/types": "^2.0.2", "find-up": "5.0.0", "got": "12.5.3", "js-yaml": "4.1.0", diff --git a/packages/cli/src/cmds/node/handler.ts b/packages/cli/src/cmds/node/handler.ts index 45784ae8..7b04dd4b 100644 --- a/packages/cli/src/cmds/node/handler.ts +++ b/packages/cli/src/cmds/node/handler.ts @@ -66,6 +66,8 @@ export async function nodeHandler(args: IGlobalArgs): Promise { // address: params.api["address"], // cors: params.api["cors"], // enableRequestLogging: params.api["enableRequestLogging"], + // ws: params.api["ws"], + // wsPort: params.api["wsPort"], // }, // network: initNetworkOptions(enr, params.p2p, params.dataDir), // }; @@ -119,6 +121,8 @@ export async function getNodeConfigFromArgs(args: IGlobalArgs): Promise<{ port: entries.get("api.port"), cors: entries.get("api.cors"), enableRequestLogging: entries.get("api.enableRequestLogging"), + ws: entries.get("api.ws"), + wsPort: entries.get("api.wsPort"), }, executor: { bundlingMode: entries.get("executor.bundlingMode"), diff --git a/packages/cli/src/cmds/standalone/handler.ts b/packages/cli/src/cmds/standalone/handler.ts index 37855d22..1156d49f 100644 --- a/packages/cli/src/cmds/standalone/handler.ts +++ b/packages/cli/src/cmds/standalone/handler.ts @@ -81,6 +81,8 @@ export async function bundlerHandler( port: args["api.port"], host: args["api.address"], cors: args["api.cors"], + ws: args["api.ws"], + wsPort: args["api.wsPort"], }); const metrics = args["metrics.enable"] @@ -123,8 +125,8 @@ export async function bundlerHandler( : null; new ApiApp({ - server: server.application, - config: config, + server, + config, testingMode, redirectRpc, executor, diff --git a/packages/cli/src/options/bundlerOptions/api.ts b/packages/cli/src/options/bundlerOptions/api.ts index ccf9fb60..0ad5a759 100644 --- a/packages/cli/src/options/bundlerOptions/api.ts +++ b/packages/cli/src/options/bundlerOptions/api.ts @@ -7,6 +7,8 @@ export interface IApiArgs { "api.address": string; "api.port": number; "api.enableRequestLogging": boolean; + "api.ws": boolean; + "api.wsPort": number; } export function parseArgs(args: IApiArgs): IBundlerOptions["api"] { @@ -15,6 +17,8 @@ export function parseArgs(args: IApiArgs): IBundlerOptions["api"] { port: args["api.port"], cors: args["api.cors"], enableRequestLogging: args["api.enableRequestLogging"], + ws: args["api.ws"], + wsPort: args["api.wsPort"], }; } @@ -51,4 +55,20 @@ export const options: ICliCommandOptions = { group: "api", demandOption: false, }, + + "api.ws": { + type: "boolean", + description: "Enable websocket interface", + default: defaultApiOptions.ws, + group: "api", + demandOption: false, + }, + + "api.wsPort": { + type: "number", + description: "Enable websocket interface", + default: defaultApiOptions.wsPort, + group: "api", + demandOption: false, + }, }; diff --git a/packages/db/package.json b/packages/db/package.json index d234f9b5..c870b7af 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "description": "The DB module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://github.com/etherspot/etherspot-bundler#readme", @@ -34,7 +34,7 @@ "dependencies": { "@chainsafe/ssz": "0.10.1", "@farcaster/rocksdb": "5.5.0", - "@skandha/types": "^2.0.1" + "@skandha/types": "^2.0.2" }, "devDependencies": { "@types/rocksdb": "3.0.1", diff --git a/packages/executor/package.json b/packages/executor/package.json index 1caf65e9..6b054cde 100644 --- a/packages/executor/package.json +++ b/packages/executor/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "description": "The Relayer module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -35,10 +35,16 @@ }, "dependencies": { "@flashbots/ethers-provider-bundle": "0.6.2", - "@skandha/monitoring": "^2.0.1", - "@skandha/params": "^2.0.1", - "@skandha/types": "^2.0.1", + "@skandha/monitoring": "^2.0.2", + "@skandha/params": "^2.0.2", + "@skandha/types": "^2.0.2", + "@skandha/utils": "^2.0.2", "async-mutex": "0.4.0", - "ethers": "5.7.2" + "ethers": "5.7.2", + "strict-event-emitter-types": "2.0.0", + "ws": "8.16.0" + }, + "devDependencies": { + "@types/ws": "8.2.2" } } diff --git a/packages/executor/src/config.ts b/packages/executor/src/config.ts index 6d4ee283..e0173222 100644 --- a/packages/executor/src/config.ts +++ b/packages/executor/src/config.ts @@ -21,7 +21,13 @@ export class Config { static async init(configOptions: ConfigOptions): Promise { const config = new Config(configOptions); - await config.fetchChainId(); + try { + await config.fetchChainId(); + } catch (err) { + // trying again with skipping ssl errors + process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; + await config.fetchChainId(); + } return config; } @@ -82,7 +88,7 @@ export class Config { if (config == null) config = {} as NetworkConfig; config.entryPoints = fromEnvVar( "ENTRYPOINTS", - config.entryPoints || [], + config.entryPoints ?? [], true ) as string[]; @@ -148,13 +154,7 @@ export class Config { config.useropsTTL || bundlerDefaultConfigs.useropsTTL ) ); - config.estimationStaticBuffer = Number( - fromEnvVar( - "ESTIMATION_STATIC_BUFFER", - config.estimationStaticBuffer || - bundlerDefaultConfigs.estimationStaticBuffer - ) - ); + config.minStake = BigNumber.from( fromEnvVar("MIN_STAKE", config.minStake ?? bundlerDefaultConfigs.minStake) ); @@ -211,6 +211,114 @@ export class Config { ) ); + config.gasFeeInSimulation = Boolean( + fromEnvVar( + "GAS_FEE_IN_SIMULATION", + config.gasFeeInSimulation || bundlerDefaultConfigs.gasFeeInSimulation + ) + ); + + config.throttlingSlack = Number( + fromEnvVar( + "THROTTLING_SLACK", + config.throttlingSlack || bundlerDefaultConfigs.throttlingSlack + ) + ); + + config.banSlack = Number( + fromEnvVar("BAN_SLACK", config.banSlack || bundlerDefaultConfigs.banSlack) + ); + + config.minInclusionDenominator = Number( + fromEnvVar( + "MIN_INCLUSION_DENOMINATOR", + config.minInclusionDenominator || + bundlerDefaultConfigs.minInclusionDenominator + ) + ); + + config.skipBundleValidation = Boolean( + fromEnvVar( + "SKIP_BUNDLE_VALIDATION", + config.skipBundleValidation || + bundlerDefaultConfigs.skipBundleValidation + ) + ); + + config.bundleGasLimit = Number( + fromEnvVar( + "BUNDLE_GAS_LIMIT", + config.bundleGasLimit || bundlerDefaultConfigs.bundleGasLimit + ) + ); + + config.userOpGasLimit = Number( + fromEnvVar( + "USEROP_GAS_LIMIT", + config.userOpGasLimit || bundlerDefaultConfigs.userOpGasLimit + ) + ); + + config.merkleApiURL = String( + fromEnvVar( + "MERKLE_API_URL", + config.merkleApiURL || bundlerDefaultConfigs.merkleApiURL + ) + ); + + config.kolibriAuthKey = String( + fromEnvVar( + "KOLIBRI_AUTH_KEY", + config.kolibriAuthKey || bundlerDefaultConfigs.kolibriAuthKey + ) + ); + + config.cglMarkup = Number( + fromEnvVar( + "CGL_MARKUP", + config.cglMarkup || bundlerDefaultConfigs.cglMarkup + ) + ); + + config.vglMarkup = Number( + fromEnvVar( + "VGL_MARKUP", + config.vglMarkup || bundlerDefaultConfigs.vglMarkup + ) + ); + + config.fastlaneValidators = fromEnvVar( + "FASTLANE_VALIDATOR", + config.fastlaneValidators ?? bundlerDefaultConfigs.fastlaneValidators, + true + ) as string[]; + + config.archiveDuration = Number( + fromEnvVar( + "ARCHIVE_DURATION", + config.archiveDuration || bundlerDefaultConfigs.archiveDuration + ) + ); + + config.pvgMarkupPercent = Number( + fromEnvVar( + "PVG_MARKUP_PERCENT", + config.pvgMarkupPercent || bundlerDefaultConfigs.pvgMarkupPercent + ) + ); + config.cglMarkupPercent = Number( + fromEnvVar( + "CGL_MARKUP_PERCENT", + config.cglMarkupPercent || bundlerDefaultConfigs.cglMarkupPercent + ) + ); + config.vglMarkupPercent = Number( + fromEnvVar( + "VGL_MARKUP_PERCENT", + config.vglMarkupPercent || bundlerDefaultConfigs.vglMarkupPercent + ) + ); + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!config.whitelistedEntities) { config.whitelistedEntities = bundlerDefaultConfigs.whitelistedEntities; @@ -266,9 +374,24 @@ const bundlerDefaultConfigs: BundlerConfig = { bundleInterval: 10000, // 10 seconds bundleSize: 4, // max size of bundle (in terms of user ops) relayingMode: "classic", - pvgMarkup: 0, canonicalMempoolId: "", canonicalEntryPoint: "", + gasFeeInSimulation: false, + skipBundleValidation: false, + userOpGasLimit: 25000000, + bundleGasLimit: 25000000, + merkleApiURL: "https://pool.merkle.io", + kolibriAuthKey: "", + cglMarkup: 35000, + vglMarkup: 0, + pvgMarkup: 0, + echoAuthKey: "", + fastlaneValidators: [], + archiveDuration: 24 * 3600, + estimationGasLimit: 0, + pvgMarkupPercent: 0, + cglMarkupPercent: 0, + vglMarkupPercent: 3000, // 30% }; function getEnvVar(envVar: string, fallback: T): T | string { diff --git a/packages/executor/src/entities/MempoolEntry.ts b/packages/executor/src/entities/MempoolEntry.ts index fad96f8b..db2faa6d 100644 --- a/packages/executor/src/entities/MempoolEntry.ts +++ b/packages/executor/src/entities/MempoolEntry.ts @@ -19,8 +19,11 @@ export class MempoolEntry implements IMempoolEntry { userOpHash: string; status: MempoolEntryStatus; hash?: string; // keccak256 of all referenced contracts - transaction?: string; // hash of a submitted bundle submitAttempts: number; + submittedTime?: number; // timestamp when mempool was first put into the mempool + transaction?: string; // transaction hash of a submitted bundle + actualTransaction?: string; // hash of an actual transaction (in case the original tx was front-runned) + revertReason?: string; constructor({ chainId, @@ -36,6 +39,9 @@ export class MempoolEntry implements IMempoolEntry { status, transaction, submitAttempts, + submittedTime, + actualTransaction, + revertReason, }: { chainId: number; userOp: UserOperation; @@ -50,6 +56,9 @@ export class MempoolEntry implements IMempoolEntry { status?: MempoolEntryStatus | undefined; transaction?: string | undefined; submitAttempts?: number | undefined; + submittedTime?: number | undefined; + actualTransaction?: string | undefined; + revertReason?: string | undefined; }) { this.chainId = chainId; this.userOp = userOp; @@ -61,9 +70,12 @@ export class MempoolEntry implements IMempoolEntry { this.paymaster = paymaster; this.hash = hash; this.lastUpdatedTime = lastUpdatedTime ?? now(); + this.submittedTime = submittedTime; this.status = status ?? MempoolEntryStatus.New; this.transaction = transaction; this.submitAttempts = submitAttempts ?? 0; + this.actualTransaction = actualTransaction; + this.revertReason = revertReason; this.validateAndTransformUserOp(); } @@ -71,10 +83,35 @@ export class MempoolEntry implements IMempoolEntry { * Set status of an entry * If status is Pending, transaction hash is required */ - setStatus(status: MempoolEntryStatus, transaction?: string): void { + setStatus( + status: MempoolEntryStatus, + params?: { + transaction?: string; + revertReason?: string; + } + ): void { this.status = status; - if (transaction) { - this.transaction = transaction; + this.lastUpdatedTime = now(); + switch (status) { + case MempoolEntryStatus.Pending: { + this.transaction = params?.transaction; + break; + } + case MempoolEntryStatus.Submitted: { + this.transaction = params?.transaction; + break; + } + case MempoolEntryStatus.OnChain: { + this.actualTransaction = params?.transaction; + break; + } + case MempoolEntryStatus.Reverted: { + this.revertReason = params?.revertReason; + break; + } + default: { + // nothing + } } } @@ -86,6 +123,7 @@ export class MempoolEntry implements IMempoolEntry { * @returns boolaen */ canReplace(existingEntry: MempoolEntry): boolean { + if (existingEntry.status > MempoolEntryStatus.OnChain) return true; if (!this.isEqual(existingEntry)) return false; if ( BigNumber.from(this.userOp.maxPriorityFeePerGas).lt( @@ -191,6 +229,9 @@ export class MempoolEntry implements IMempoolEntry { transaction: this.transaction, submitAttempts: this.submitAttempts, status: this.status, + submittedTime: this.submittedTime, + actualTransaction: this.actualTransaction, + revertReason: this.revertReason, }; } } diff --git a/packages/executor/src/entities/interfaces.ts b/packages/executor/src/entities/interfaces.ts index 823dc77a..e85b7b6f 100644 --- a/packages/executor/src/entities/interfaces.ts +++ b/packages/executor/src/entities/interfaces.ts @@ -1,6 +1,9 @@ import { BigNumberish, BytesLike } from "ethers"; import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; -import { MempoolEntryStatus, ReputationStatus } from "@skandha/types/lib/executor"; +import { + MempoolEntryStatus, + ReputationStatus, +} from "@skandha/types/lib/executor"; export interface IMempoolEntry { chainId: number; @@ -16,6 +19,9 @@ export interface IMempoolEntry { status: MempoolEntryStatus; transaction?: string; submitAttempts: number; + submittedTime?: number; + actualTransaction?: string; + revertReason?: string; } export interface MempoolEntrySerialized { @@ -47,6 +53,9 @@ export interface MempoolEntrySerialized { transaction: string | undefined; submitAttempts: number; status: MempoolEntryStatus; + submittedTime?: number; + actualTransaction: string | undefined; + revertReason: string | undefined; } export interface IReputationEntry { diff --git a/packages/executor/src/executor.ts b/packages/executor/src/executor.ts index 7dc69897..cd8f6fc0 100644 --- a/packages/executor/src/executor.ts +++ b/packages/executor/src/executor.ts @@ -12,6 +12,9 @@ import { ReputationService, P2PService, EntryPointService, + EventsService, + ExecutorEventBus, + SubscriptionService, } from "./services"; import { Config } from "./config"; import { BundlingMode, GetNodeAPI, NetworkConfig } from "./interfaces"; @@ -48,6 +51,12 @@ export class Executor { public userOpValidationService: UserOpValidationService; public reputationService: ReputationService; public p2pService: P2PService; + // eventsService listens for events in the blockchain and deletes userop from mempool, manages reputation, etc... + public eventsService: EventsService; + // eventBus is used to propagate different events across executor service + public eventBus: ExecutorEventBus; + // ws subscription service listens the eventBus and sends event to ws listeners + public subscriptionService: SubscriptionService; private db: IDbController; @@ -66,6 +75,12 @@ export class Executor { this.provider = this.config.getNetworkProvider(); + this.eventBus = new ExecutorEventBus(); + this.subscriptionService = new SubscriptionService( + this.eventBus, + this.logger + ); + this.reputationService = new ReputationService( this.db, this.chainId, @@ -75,15 +90,36 @@ export class Executor { BigNumber.from(this.networkConfig.minStake), this.networkConfig.minUnstakeDelay ); + this.entryPointService = new EntryPointService( this.chainId, this.networkConfig, this.provider, - this.reputationService, this.db, this.logger ); + + this.mempoolService = new MempoolService( + this.db, + this.chainId, + this.entryPointService, + this.reputationService, + this.eventBus, + this.networkConfig, + this.logger + ); + + this.skandha = new Skandha( + this.mempoolService, + this.entryPointService, + this.chainId, + this.provider, + this.config, + this.logger + ); + this.userOpValidationService = new UserOpValidationService( + this.skandha, this.provider, this.entryPointService, this.reputationService, @@ -91,13 +127,18 @@ export class Executor { this.config, this.logger ); - this.mempoolService = new MempoolService( - this.db, + + this.eventsService = new EventsService( this.chainId, - this.entryPointService, + this.networkConfig, this.reputationService, - this.networkConfig + this.mempoolService, + this.entryPointService, + this.eventBus, + this.db, + this.logger ); + this.bundlingService = new BundlingService( this.chainId, this.provider, @@ -105,6 +146,7 @@ export class Executor { this.mempoolService, this.userOpValidationService, this.reputationService, + this.eventBus, this.config, this.logger, this.metrics, @@ -120,13 +162,7 @@ export class Executor { this.reputationService, this.networkConfig ); - this.skandha = new Skandha( - this.entryPointService, - this.chainId, - this.provider, - this.config, - this.logger - ); + this.eth = new Eth( this.chainId, this.provider, @@ -139,6 +175,7 @@ export class Executor { this.metrics, this.getNodeApi ); + this.p2pService = new P2PService( this.entryPointService, this.mempoolService @@ -152,14 +189,6 @@ export class Executor { this.bundlingService.setMaxBundleSize(10); } - 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("[X] FLASHBOTS BUIDLER API"); - } - if (this.networkConfig.conditionalTransactions) { this.logger.info("[x] CONDITIONAL TRANSACTIONS"); } diff --git a/packages/executor/src/interfaces.ts b/packages/executor/src/interfaces.ts index 36a5c409..9f8c1cef 100644 --- a/packages/executor/src/interfaces.ts +++ b/packages/executor/src/interfaces.ts @@ -1,5 +1,8 @@ import { BigNumber, BigNumberish, BytesLike } from "ethers"; -import { IWhitelistedEntities, RelayingMode } from "@skandha/types/lib/executor"; +import { + IWhitelistedEntities, + RelayingMode, +} from "@skandha/types/lib/executor"; import { INodeAPI } from "@skandha/types/lib/node"; import { MempoolEntry } from "./entities/MempoolEntry"; @@ -148,6 +151,29 @@ export interface NetworkConfig { canonicalMempoolId: string; // canonical entry point canonicalEntryPoint: string; + // add gas fee in simulated transactions (may be required for some rpc providers) + gasFeeInSimulation: boolean; + // skips bundle validation + skipBundleValidation: boolean; + userOpGasLimit: number; // 25kk by default + bundleGasLimit: number; // 25kk by default + // api url of Merkle.io (by default https://pool.merkle.io) + merkleApiURL: string; + kolibriAuthKey: string; + // adds certain amount of gas to callGasLimit + // 35000 by default + cglMarkup: number; + // adds certain amount of gas to verificationGasLimit + // 35000 by default + vglMarkup: number; + // api auth key for echo: https://echo.chainbound.io/docs/usage/api-interface#authentication + echoAuthKey: string; + fastlaneValidators: string[]; + archiveDuration: number; + estimationGasLimit: number; + pvgMarkupPercent: number; + cglMarkupPercent: number; + vglMarkupPercent: number; } export type BundlerConfig = Omit< diff --git a/packages/executor/src/modules/debug.ts b/packages/executor/src/modules/debug.ts index 8da8cd50..869decca 100644 --- a/packages/executor/src/modules/debug.ts +++ b/packages/executor/src/modules/debug.ts @@ -10,8 +10,11 @@ import { MempoolService, ReputationService, } from "../services"; +import { + MempoolEntrySerialized, + ReputationEntryDump, +} from "../entities/interfaces"; import { BundlingMode, GetStakeStatus, NetworkConfig } from "../interfaces"; -import { ReputationEntryDump } from "../entities/interfaces"; import { SetReputationArgs, SetMempoolArgs } from "./interfaces"; /* SPEC: https://eips.ethereum.org/EIPS/eip-4337#rpc-methods-debug-namespace @@ -69,6 +72,15 @@ export class Debug { .map((entry) => entry.userOp); } + /** + * Dumps the current UserOperations mempool + * array - Array of UserOperations currently in the mempool + */ + async dumpMempoolRaw(): Promise { + const entries = await this.mempoolService.dump(); + return entries.map((entry) => entry); + } + /** * Forces the bundler to build and execute a bundle from the mempool as handleOps() transaction */ diff --git a/packages/executor/src/modules/eth.ts b/packages/executor/src/modules/eth.ts index c55ef828..e6b46d6d 100644 --- a/packages/executor/src/modules/eth.ts +++ b/packages/executor/src/modules/eth.ts @@ -14,12 +14,12 @@ import { estimateMantlePVG, AddressZero, serializeMempoolId, - estimateAncient8PVG, } from "@skandha/params/lib"; import { Logger } from "@skandha/types/lib"; import { PerChainMetrics } from "@skandha/monitoring/lib"; import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; import { UserOperationStruct } from "@skandha/types/lib/contracts/EPv6/EntryPoint"; +import { MempoolEntryStatus } from "@skandha/types/lib/executor"; import { UserOpValidationService, MempoolService, @@ -27,6 +27,7 @@ import { } from "../services"; import { GetNodeAPI, NetworkConfig } from "../interfaces"; import { EntryPointVersion } from "../services/EntryPointService/interfaces"; +import { getUserOpGasLimit } from "../services/BundlingService/utils"; import { EstimateUserOperationGasArgs, SendUserOperationGasArgs, @@ -53,19 +54,15 @@ export class Eth { this.pvgEstimator = estimateArbitrumPVG(this.provider); } - // ["optimism", "optimismGoerli"] - if ([10, 420].includes(this.chainId)) { + // ["optimism", "optimismGoerli", "base", "ancient8"] + if ([10, 420, 8453, 888888888].includes(this.chainId)) { this.pvgEstimator = estimateOptimismPVG(this.provider); } - // mantle - if ([5000, 5001].includes(this.chainId)) { + // mantle, mantle testnet, mantle sepolia + if ([5000, 5001, 5003].includes(this.chainId)) { this.pvgEstimator = estimateMantlePVG(this.provider); } - - if ([888888888].includes(this.chainId)) { - this.pvgEstimator = estimateAncient8PVG(this.provider); - } } /** @@ -82,6 +79,12 @@ export class Eth { await this.mempoolService.validateUserOpReplaceability(userOp, entryPoint); this.logger.debug("Validating user op before sending to mempool..."); + if (getUserOpGasLimit(userOp).gt(this.config.userOpGasLimit)) { + throw new RpcError( + "UserOp's gas limit is too high", + RpcErrorCodes.INVALID_USEROP + ); + } await this.userOpValidationService.validateGasFee(userOp); const validationResult = await this.userOpValidationService.simulateValidation(userOp, entryPoint); @@ -150,14 +153,13 @@ export class Eth { async estimateUserOperationGas( args: EstimateUserOperationGasArgs ): Promise { - const userOp = args.userOp; - const entryPoint = args.entryPoint.toLowerCase(); + const { userOp: partialUserOp, entryPoint } = args; if (!this.validateEntryPoint(entryPoint)) { throw new RpcError("Invalid Entrypoint", RpcErrorCodes.INVALID_REQUEST); } - const userOpComplemented: UserOperation = { - ...userOp, + const userOp: UserOperation = { + ...partialUserOp, callGasLimit: BigNumber.from(10e6), preVerificationGas: BigNumber.from(1e6), verificationGasLimit: BigNumber.from(10e6), @@ -165,12 +167,18 @@ export class Eth { maxPriorityFeePerGas: 1, }; - if (userOpComplemented.signature.length <= 2) { - userOpComplemented.signature = ECDSA_DUMMY_SIGNATURE; + if (this.chainId == 80002) { + userOp.callGasLimit = BigNumber.from(20e6); + userOp.preVerificationGas = BigNumber.from(50000); + userOp.verificationGasLimit = BigNumber.from(3e6); + } + + if (userOp.signature.length <= 2) { + userOp.signature = ECDSA_DUMMY_SIGNATURE; } const returnInfo = await this.userOpValidationService.validateForEstimation( - userOpComplemented, + userOp, entryPoint ); @@ -178,26 +186,23 @@ export class Eth { let { preOpGas, validAfter, validUntil, paid } = returnInfo; const verificationGasLimit = BigNumber.from(preOpGas) - .sub(userOpComplemented.preVerificationGas) - .mul(130) - .div(100) // 130% markup + .sub(userOp.preVerificationGas) + .mul(10000 + this.config.vglMarkupPercent) + .div(10000) // % markup + .add(this.config.vglMarkup) .toNumber(); - let preVerificationGas: BigNumberish = - this.entryPointService.calcPreverificationGas( - entryPoint, - userOpComplemented - ); - userOpComplemented.preVerificationGas = preVerificationGas; - let callGasLimit: BigNumber = BigNumber.from(0); - // calculate callGasLimit based on paid fee - const { estimationStaticBuffer } = this.config; - callGasLimit = BigNumber.from(paid).div(userOpComplemented.maxFeePerGas); - callGasLimit = callGasLimit.sub(preOpGas).add(estimationStaticBuffer || 0); - - if (callGasLimit.lt(0)) { - callGasLimit = BigNumber.from(estimationStaticBuffer || 0); + const { cglMarkup } = this.config; + const totalGas: BigNumber = BigNumber.from(paid).div(userOp.maxFeePerGas); + let callGasLimit = totalGas + .sub(preOpGas) + .mul(10000 + this.config.cglMarkupPercent) + .div(10000) // % markup + .add(cglMarkup || 0); + + if (callGasLimit.lt(cglMarkup)) { + callGasLimit = BigNumber.from(cglMarkup); } //< checking for execution revert @@ -214,22 +219,17 @@ export class Eth { }); //> - // Binary search gas limits - const userOpToEstimate: UserOperation = { - ...userOpComplemented, - preVerificationGas, - verificationGasLimit, - callGasLimit, - }; - + userOp.callGasLimit = callGasLimit; + let preVerificationGas: BigNumberish = + this.entryPointService.calcPreverificationGas(entryPoint, userOp); const gasFee = await this.skandhaModule.getGasPrice(); if (this.pvgEstimator) { - userOpComplemented.maxFeePerGas = gasFee.maxFeePerGas; - userOpComplemented.maxPriorityFeePerGas = gasFee.maxPriorityFeePerGas; + userOp.maxFeePerGas = gasFee.maxFeePerGas; + userOp.maxPriorityFeePerGas = gasFee.maxPriorityFeePerGas; const data = this.entryPointService.encodeHandleOps( entryPoint, - [userOpComplemented], + [userOp], AddressZero ); preVerificationGas = await this.pvgEstimator( @@ -240,20 +240,23 @@ export class Eth { contractCreation: Boolean( userOp.factory && userOp.factory.length > 2 ), - userOp: userOpComplemented, + userOp, } ); } + preVerificationGas = BigNumber.from(preVerificationGas) + .mul(10000 + this.config.pvgMarkupPercent) + .div(10000); this.metrics?.useropsEstimated.inc(); return { preVerificationGas, - verificationGasLimit: userOpToEstimate.verificationGasLimit, - verificationGas: userOpToEstimate.verificationGasLimit, + verificationGasLimit, + verificationGas: verificationGasLimit, validAfter: validAfter ? BigNumber.from(validAfter) : undefined, validUntil: validUntil ? BigNumber.from(validUntil) : undefined, - callGasLimit: userOpToEstimate.callGasLimit, + callGasLimit, maxFeePerGas: gasFee.maxFeePerGas, maxPriorityFeePerGas: gasFee.maxPriorityFeePerGas, }; @@ -346,6 +349,20 @@ export class Eth { async getUserOperationByHash( hash: string ): Promise { + const entry = await this.mempoolService.getEntryByHash(hash); + if (entry && entry.status < MempoolEntryStatus.Submitted) { + let transaction: Partial = {}; + if (entry.transaction) { + transaction = await this.provider.getTransaction(entry.transaction); + } + return { + userOperation: entry.userOp, + entryPoint: entry.entryPoint, + transactionHash: transaction.hash, + blockHash: transaction.blockHash, + blockNumber: transaction.blockNumber, + }; + } return this.entryPointService.getUserOperationByHash(hash); } diff --git a/packages/executor/src/modules/skandha.ts b/packages/executor/src/modules/skandha.ts index 341a3f58..f009d619 100644 --- a/packages/executor/src/modules/skandha.ts +++ b/packages/executor/src/modules/skandha.ts @@ -9,10 +9,12 @@ import RpcError from "@skandha/types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; import { GasPriceMarkupOne } from "@skandha/params/lib"; import { getGasFee } from "@skandha/params/lib"; -import { UserOperationStruct } from "@skandha/types/lib/contracts/EPv6/EntryPoint"; +import { UserOperationStatus } from "@skandha/types/lib/api/interfaces"; +import { MempoolEntryStatus } from "@skandha/types/lib/executor"; +import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; import { NetworkConfig } from "../interfaces"; import { Config } from "../config"; -import { EntryPointService } from "../services"; +import { EntryPointService, MempoolService } from "../services"; import { EntryPointVersion } from "../services/EntryPointService/interfaces"; // custom features of Skandha @@ -20,6 +22,7 @@ export class Skandha { networkConfig: NetworkConfig; constructor( + private mempoolService: MempoolService, private entryPointService: EntryPointService, private chainId: number, private provider: ethers.providers.JsonRpcProvider, @@ -82,7 +85,7 @@ export class Skandha { testingMode: this.config.testingMode, redirectRpc: this.config.redirectRpc, }, - entryPoints: this.networkConfig.entryPoints || [], + entryPoints: this.networkConfig.entryPoints, beneficiary: this.networkConfig.beneficiary, relayers: walletAddresses, minInclusionDenominator: BigNumber.from( @@ -92,10 +95,11 @@ export class Skandha { this.networkConfig.throttlingSlack ).toNumber(), banSlack: BigNumber.from(this.networkConfig.banSlack).toNumber(), + minStake: this.networkConfig.minStake, + minUnstakeDelay: this.networkConfig.minUnstakeDelay, minSignerBalance: `${ethers.utils.formatEther( this.networkConfig.minSignerBalance )} eth`, - minStake: `${ethers.utils.formatEther(this.networkConfig.minStake!)} eth`, multicall: this.networkConfig.multicall, estimationStaticBuffer: BigNumber.from( this.networkConfig.estimationStaticBuffer @@ -123,10 +127,22 @@ export class Skandha { relayingMode: this.networkConfig.relayingMode, bundleInterval: this.networkConfig.bundleInterval, bundleSize: this.networkConfig.bundleSize, - minUnstakeDelay: this.networkConfig.minUnstakeDelay, - pvgMarkup: this.networkConfig.pvgMarkup, canonicalMempoolId: this.networkConfig.canonicalMempoolId, canonicalEntryPoint: this.networkConfig.canonicalEntryPoint, + gasFeeInSimulation: this.networkConfig.gasFeeInSimulation, + skipBundleValidation: this.networkConfig.skipBundleValidation, + pvgMarkup: this.networkConfig.pvgMarkup, + cglMarkup: this.networkConfig.cglMarkup, + vglMarkup: this.networkConfig.vglMarkup, + fastlaneValidators: this.networkConfig.fastlaneValidators, + estimationGasLimit: this.networkConfig.estimationGasLimit, + archiveDuration: this.networkConfig.archiveDuration, + pvgMarkupPercent: this.networkConfig.pvgMarkupPercent, + cglMarkupPercent: this.networkConfig.cglMarkupPercent, + vglMarkupPercent: this.networkConfig.vglMarkupPercent, + userOpGasLimit: this.networkConfig.userOpGasLimit, + bundleGasLimit: this.networkConfig.bundleGasLimit, + merkleApiURL: this.networkConfig.merkleApiURL, }; } @@ -178,7 +194,7 @@ export class Skandha { BigNumber.from(event.args.actualGasCost).div(event.args.actualGasUsed) ); const userops = txDecoded - .map((handleOps) => handleOps!.ops as UserOperationStruct[]) + .map((handleOps) => handleOps!.ops as UserOperation[]) .reduce((p, c) => { return p.concat(c); }, []); @@ -193,4 +209,32 @@ export class Skandha { throw new RpcError("Unsupported EntryPoint"); } + + async getUserOperationStatus(hash: string): Promise { + const entry = await this.mempoolService.getEntryByHash(hash); + if (entry == null) { + throw new RpcError( + "UserOperation not found", + RpcErrorCodes.INVALID_REQUEST + ); + } + + const { userOp, entryPoint } = entry; + const status = + Object.keys(MempoolEntryStatus).find( + (status) => + entry.status === + MempoolEntryStatus[status as keyof typeof MempoolEntryStatus] + ) ?? "New"; + const reason = entry.revertReason; + const transaction = entry.actualTransaction ?? entry.transaction; + + return { + userOp, + entryPoint, + status, + reason, + transaction, + }; + } } diff --git a/packages/executor/src/services/BundlingService/interfaces.ts b/packages/executor/src/services/BundlingService/interfaces.ts index 4af02892..f2d4c65a 100644 --- a/packages/executor/src/services/BundlingService/interfaces.ts +++ b/packages/executor/src/services/BundlingService/interfaces.ts @@ -6,4 +6,6 @@ export type Relayer = Wallet | providers.JsonRpcSigner; export interface IRelayingMode { isLocked(): boolean; sendBundle(bundle: Bundle): Promise; + getAvailableRelayersCount(): number; + canSubmitBundle(): Promise; } diff --git a/packages/executor/src/services/BundlingService/relayers/base.ts b/packages/executor/src/services/BundlingService/relayers/base.ts index 3d33e4cc..e931a7ae 100644 --- a/packages/executor/src/services/BundlingService/relayers/base.ts +++ b/packages/executor/src/services/BundlingService/relayers/base.ts @@ -2,6 +2,7 @@ import { Mutex } from "async-mutex"; import { constants, providers, utils } from "ethers"; import { Logger } from "@skandha/types/lib"; import { PerChainMetrics } from "@skandha/monitoring/lib"; +import { MempoolEntryStatus } from "@skandha/types/lib/executor"; import { Config } from "../../../config"; import { Bundle, NetworkConfig } from "../../../interfaces"; import { IRelayingMode, Relayer } from "../interfaces"; @@ -9,6 +10,7 @@ import { MempoolEntry } from "../../../entities/MempoolEntry"; import { now } from "../../../utils"; import { MempoolService } from "../../MempoolService"; import { ReputationService } from "../../ReputationService"; +import { ExecutorEventBus } from "../../SubscriptionService"; import { EntryPointService } from "../../EntryPointService"; const WAIT_FOR_TX_MAX_RETRIES = 3; // 3 blocks @@ -26,6 +28,7 @@ export abstract class BaseRelayer implements IRelayingMode { protected entryPointService: EntryPointService, protected mempoolService: MempoolService, protected reputationService: ReputationService, + protected eventBus: ExecutorEventBus, protected metrics: PerChainMetrics | null ) { const relayers = this.config.getRelayers(); @@ -42,29 +45,38 @@ export abstract class BaseRelayer implements IRelayingMode { throw new Error("Method not implemented."); } + getAvailableRelayersCount(): number { + return this.mutexes.filter((mutex) => !mutex.isLocked()).length; + } + + async canSubmitBundle(): Promise { + return true; + } + /** - * waits for transaction - * @param hash transaction hash - * @returns false if transaction reverted + * waits entries to get submitted + * @param hashes user op hashes array */ - protected async waitForTransaction(hash: string): Promise { - if (!utils.isHexString(hash)) return false; + protected async waitForEntries(entries: MempoolEntry[]): Promise { let retries = 0; + if (entries.length == 0) return; 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) { + if (retries >= WAIT_FOR_TX_MAX_RETRIES) { clearInterval(interval); - try { - await response.wait(0); - resolve(true); - } catch (err) { - reject(err); - } + return reject(false); + } + retries++; + for (const entry of entries) { + const exists = await this.mempoolService.find(entry); + // if some entry exists in the mempool, it means that the EventService did not delete it yet + // because that service has not received UserOperationEvent yet + // so we wait for it to get submitted... + if (exists && exists.status < MempoolEntryStatus.OnChain) return; } - }, 1000); + clearInterval(interval); + resolve(); + }, this.networkConfig.bundleInterval); }); } @@ -102,7 +114,14 @@ export abstract class BaseRelayer implements IRelayingMode { } else { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (failedEntry) { - await this.mempoolService.remove(failedEntry); + this.logger.debug(`${failedEntry.hash} reverted on chain. Deleting...`); + await this.mempoolService.updateStatus( + [failedEntry], + MempoolEntryStatus.Reverted, + { + revertReason: reason, + } + ); this.logger.error( `Failed handleOps sender=${failedEntry.userOp.sender}` ); @@ -113,15 +132,25 @@ export abstract class BaseRelayer implements IRelayingMode { // metrics protected reportSubmittedUserops(txHash: string, bundle: Bundle): void { if (txHash && this.metrics) { + this.metrics.bundlesSubmitted.inc(1); this.metrics.useropsSubmitted.inc(bundle.entries.length); + this.metrics.useropsInBundle.observe(bundle.entries.length); bundle.entries.forEach((entry) => { this.metrics!.useropsTimeToProcess.observe( - now() - entry.lastUpdatedTime + Math.ceil( + (now() - (entry.submittedTime ?? entry.lastUpdatedTime)) / 1000 + ) ); }); } } + protected reportFailedBundle(): void { + if (this.metrics) { + this.metrics.bundlesFailed.inc(1); + } + } + /** * 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. @@ -144,4 +173,73 @@ export abstract class BaseRelayer implements IRelayingMode { } return beneficiary; } + + /** + * calls eth_estimateGas with whole bundle + */ + protected async validateBundle( + relayer: Relayer, + entries: MempoolEntry[], + transactionRequest: providers.TransactionRequest + ): Promise { + if (this.networkConfig.skipBundleValidation) return true; + 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); + return true; + } catch (err) { + this.logger.debug( + `${entries + .map((entry) => entry.userOpHash) + .join("; ")} failed on chain estimation. deleting...` + ); + this.logger.error(err); + await this.setCancelled(entries, "could not estimate bundle"); + this.reportFailedBundle(); + return false; + } + } + + protected async setSubmitted( + entries: MempoolEntry[], + transaction: string + ): Promise { + await this.mempoolService.updateStatus( + entries, + MempoolEntryStatus.Submitted, + { + transaction, + } + ); + } + + protected async setCancelled( + entries: MempoolEntry[], + reason: string + ): Promise { + await this.mempoolService.updateStatus( + entries, + MempoolEntryStatus.Cancelled, + { revertReason: reason } + ); + } + + protected async setReverted( + entries: MempoolEntry[], + reason: string + ): Promise { + await this.mempoolService.updateStatus( + entries, + MempoolEntryStatus.Reverted, + { + revertReason: reason, + } + ); + } + + protected async setNew(entries: MempoolEntry[]): Promise { + await this.mempoolService.updateStatus(entries, MempoolEntryStatus.New); + } } diff --git a/packages/executor/src/services/BundlingService/relayers/classic.ts b/packages/executor/src/services/BundlingService/relayers/classic.ts index fb3ac312..dda5d543 100644 --- a/packages/executor/src/services/BundlingService/relayers/classic.ts +++ b/packages/executor/src/services/BundlingService/relayers/classic.ts @@ -1,51 +1,26 @@ import { providers } from "ethers"; -import { Logger } from "@skandha/types/lib"; -import { PerChainMetrics } from "@skandha/monitoring/lib"; import { chainsWithoutEIP1559 } from "@skandha/params/lib"; import { AccessList } from "ethers/lib/utils"; -import { MempoolEntryStatus } from "@skandha/types/lib/executor"; import { Relayer } from "../interfaces"; -import { Config } from "../../../config"; -import { Bundle, NetworkConfig, StorageMap } from "../../../interfaces"; -import { MempoolService } from "../../MempoolService"; +import { Bundle, StorageMap } from "../../../interfaces"; import { estimateBundleGasLimit } from "../utils"; -import { ReputationService } from "../../ReputationService"; -import { EntryPointService } from "../../EntryPointService"; import { BaseRelayer } from "./base"; export class ClassicRelayer extends BaseRelayer { - constructor( - logger: Logger, - chainId: number, - provider: providers.JsonRpcProvider, - config: Config, - networkConfig: NetworkConfig, - entryPointService: EntryPointService, - mempoolService: MempoolService, - reputationService: ReputationService, - metrics: PerChainMetrics | null - ) { - super( - logger, - chainId, - provider, - config, - networkConfig, - entryPointService, - mempoolService, - reputationService, - metrics - ); - } - async sendBundle(bundle: Bundle): Promise { const availableIndex = this.getAvailableRelayerIndex(); - if (availableIndex == null) return; + if (availableIndex == null) { + this.logger.error("Relayer: No available relayers"); + return; + } const relayer = this.relayers[availableIndex]; const mutex = this.mutexes[availableIndex]; const { entries, storageMap } = bundle; - if (!bundle.entries.length) return; + if (!bundle.entries.length) { + this.logger.error("Relayer: Bundle is empty"); + return; + } await mutex.runExclusive(async (): Promise => { const beneficiary = await this.selectBeneficiary(relayer); @@ -97,7 +72,8 @@ export class ClassicRelayer extends BaseRelayer { ...transactionRequest, gasLimit: estimateBundleGasLimit( this.networkConfig.bundleGasLimitMarkup, - bundle.entries + bundle.entries, + this.networkConfig.estimationGasLimit ), chainId: this.provider._network.chainId, nonce: await relayer.getTransactionCount(), @@ -106,49 +82,59 @@ export class ClassicRelayer extends BaseRelayer { // 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; + + if (this.chainId == 5003) { + const { gasLimit: _, ...txWithoutGasLimit } = transactionRequest; + transaction.gasLimit = await relayer.estimateGas(txWithoutGasLimit); + } else { + if ( + !(await this.validateBundle(relayer, entries, transactionRequest)) + ) { + return; + } } + this.logger.debug( + `Trying to submit userops: ${bundle.entries + .map((entry) => entry.userOpHash) + .join(", ")}` + ); 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); + await this.setSubmitted(entries, txHash); + + await this.waitForEntries(entries) + .then(() => { + this.reportSubmittedUserops(txHash, bundle); + }) + .catch(async (err) => { + this.reportFailedBundle(); + this.logger.error(err, "Relayer: Could not find transaction"); + await this.setReverted(entries, "execution reverted"); + }); }) .catch(async (err: any) => { + this.reportFailedBundle(); // 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.setNew(entries); await this.handleUserOpFail(entries, err); }); } else { await relayer .sendTransaction(transaction) + .then(async ({ hash }) => { + this.logger.debug(`Bundle submitted: ${hash}`); + this.logger.debug( + `User op hashes ${entries.map((entry) => entry.userOpHash)}` + ); + await this.setSubmitted(entries, hash); + }) .catch((err: any) => this.handleUserOpFail(entries, err)); - await this.mempoolService.removeAll(entries); } }); } diff --git a/packages/executor/src/services/BundlingService/relayers/echo.ts b/packages/executor/src/services/BundlingService/relayers/echo.ts new file mode 100644 index 00000000..362c590b --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/echo.ts @@ -0,0 +1,189 @@ +import { providers } from "ethers"; +import { PerChainMetrics } from "@skandha/monitoring/lib"; +import { Logger } from "@skandha/types/lib"; +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 { ExecutorEventBus } from "../../SubscriptionService"; +import { EntryPointService } from "../../EntryPointService"; +import { BaseRelayer } from "./base"; + +export class EchoRelayer extends BaseRelayer { + private submitTimeout = 5 * 60 * 1000; // 5 minutes + + constructor( + logger: Logger, + chainId: number, + provider: providers.JsonRpcProvider, + config: Config, + networkConfig: NetworkConfig, + entryPointService: EntryPointService, + mempoolService: MempoolService, + reputationService: ReputationService, + eventBus: ExecutorEventBus, + metrics: PerChainMetrics | null + ) { + super( + logger, + chainId, + provider, + config, + networkConfig, + entryPointService, + mempoolService, + reputationService, + eventBus, + metrics + ); + if (this.networkConfig.echoAuthKey.length === 0) { + throw new Error("Echo API key is missing"); + } + } + + async sendBundle(bundle: Bundle): 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 beneficiary = await this.selectBeneficiary(relayer); + const entryPoint = entries[0]!.entryPoint; + + const txRequest = this.entryPointService.encodeHandleOps( + entryPoint, + 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, + this.networkConfig.estimationGasLimit + ), + chainId: this.provider._network.chainId, + nonce: await relayer.getTransactionCount(), + }; + + if (!(await this.validateBundle(relayer, entries, transactionRequest))) { + return; + } + + await this.submitTransaction(relayer, transactionRequest) + .then(async (txHash) => { + this.logger.debug(`Echo: Bundle submitted: ${txHash}`); + this.logger.debug( + `Echo: User op hashes ${entries.map((entry) => entry.userOpHash)}` + ); + await this.setSubmitted(entries, txHash); + await this.waitForEntries(entries).catch((err) => + this.logger.error(err, "Echo: Could not find transaction") + ); + this.reportSubmittedUserops(txHash, bundle); + }) + .catch(async (err: any) => { + this.reportFailedBundle(); + // Put all userops back to the mempool + // if some userop failed, it will be deleted inside handleUserOpFail() + await this.setNew(entries); + if (err === "timeout") { + this.logger.debug("Echo: 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, "Echo: Submitting"); + const echoProvider = new providers.JsonRpcProvider({ + url: this.networkConfig.rpcEndpointSubmit, + headers: { + "x-api-key": this.networkConfig.echoAuthKey, + }, + }); + + 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 txsSigned = [await signer.signTransaction(transaction)]; + this.logger.debug(`Echo: Trying to submit to block ${targetBlock}`); + try { + const bundleReceipt: EchoSuccessfulResponse = await echoProvider.send( + "eth_sendBundle", + [ + { + txs: txsSigned, + blockNumber: targetBlock, + awaitReceipt: true, + usePublicMempool: false, + }, + ] + ); + this.logger.debug(bundleReceipt, "Echo: received receipt"); + lock = false; + if ( + bundleReceipt == null || + bundleReceipt.receiptNotification == null + ) { + return; // try again + } + if (bundleReceipt.receiptNotification.status === "included") { + this.provider.removeListener("block", handler); + resolve(bundleReceipt.bundleHash); + } + if (bundleReceipt.receiptNotification.status === "timedOut") { + return; // try again + } + } catch (err) { + this.logger.error(err, "Echo: received error"); + this.provider.removeListener("block", handler); + return reject(err); + } + }; + this.provider.on("block", handler); + }); + } +} + +type EchoSuccessfulResponse = { + bundleHash: string; + receiptNotification: { + status: "included" | "timedOut"; + data: { + blockNumber: number; + elapsedMs: number; + }; + }; +}; diff --git a/packages/executor/src/services/BundlingService/relayers/fastlane.ts b/packages/executor/src/services/BundlingService/relayers/fastlane.ts new file mode 100644 index 00000000..ef2bc770 --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/fastlane.ts @@ -0,0 +1,252 @@ +import { providers } from "ethers"; +import { Logger } from "@skandha/types/lib"; +import { PerChainMetrics } from "@skandha/monitoring/lib"; +import { chainsWithoutEIP1559 } from "@skandha/params/lib"; +import { AccessList } from "ethers/lib/utils"; +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 { now } from "../../../utils"; +import { ExecutorEventBus } from "../../SubscriptionService"; +import { EntryPointService } from "../../EntryPointService"; +import { BaseRelayer } from "./base"; + +export class FastlaneRelayer extends BaseRelayer { + private submitTimeout = 10 * 60 * 1000; // 10 minutes + + constructor( + logger: Logger, + chainId: number, + provider: providers.JsonRpcProvider, + config: Config, + networkConfig: NetworkConfig, + entryPointService: EntryPointService, + mempoolService: MempoolService, + reputationService: ReputationService, + eventBus: ExecutorEventBus, + metrics: PerChainMetrics | null + ) { + super( + logger, + chainId, + provider, + config, + networkConfig, + entryPointService, + mempoolService, + reputationService, + eventBus, + metrics + ); + if (!this.networkConfig.conditionalTransactions) { + throw new Error("Fastlane: You must enable conditional transactions"); + } + if (!this.networkConfig.rpcEndpointSubmit) { + throw new Error("Fastlane: You must set rpcEndpointSubmit"); + } + } + + async sendBundle(bundle: Bundle): Promise { + const availableIndex = this.getAvailableRelayerIndex(); + if (availableIndex == null) { + this.logger.error("Fastlane: No available relayers"); + return; + } + const relayer = this.relayers[availableIndex]; + const mutex = this.mutexes[availableIndex]; + + const { entries, storageMap } = bundle; + if (!bundle.entries.length) { + this.logger.error("Fastlane: Bundle is empty"); + return; + } + + await mutex.runExclusive(async (): Promise => { + const beneficiary = await this.selectBeneficiary(relayer); + const entryPoint = entries[0]!.entryPoint; + const txRequest = this.entryPointService.encodeHandleOps( + entryPoint, + 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, + this.networkConfig.estimationGasLimit + ), + chainId: this.provider._network.chainId, + nonce: await relayer.getTransactionCount(), + }; + + if (!(await this.validateBundle(relayer, entries, transactionRequest))) { + return; + } + + this.logger.debug( + `Fastlane: Trying to submit userops: ${bundle.entries + .map((entry) => entry.userOpHash) + .join(", ")}` + ); + + await this.submitTransaction(relayer, transaction, storageMap) + .then(async (txHash: string) => { + this.logger.debug(`Fastlane: Bundle submitted: ${txHash}`); + this.logger.debug( + `Fastlane: User op hashes ${entries.map( + (entry) => entry.userOpHash + )}` + ); + await this.setSubmitted(entries, txHash); + + await this.waitForEntries(entries).catch((err) => + this.logger.error(err, "Fastlane: Could not find transaction") + ); + this.reportSubmittedUserops(txHash, bundle); + }) + .catch(async (err: any) => { + this.reportFailedBundle(); + // Put all userops back to the mempool + // if some userop failed, it will be deleted inside handleUserOpFail() + await this.setNew(entries); + await this.handleUserOpFail(entries, err); + }); + }); + } + + async canSubmitBundle(): Promise { + try { + const provider = new providers.JsonRpcProvider( + "https://rpc-mainnet.maticvigil.com" + ); + const validators = await provider.send("bor_getCurrentValidators", []); + for (let fastlane of this.networkConfig.fastlaneValidators) { + fastlane = fastlane.toLowerCase(); + if ( + validators.some( + (validator: { signer: string }) => + validator.signer.toLowerCase() == fastlane + ) + ) { + return true; + } + } + } catch (err) { + this.logger.error(err, "Fastlane: error on bor_getCurrentValidators"); + } + return false; + } + + /** + * 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 = "pfl_sendRawTransactionConditional"; + + const provider = new providers.JsonRpcProvider( + this.networkConfig.rpcEndpointSubmit + ); + const submitStart = now(); + return new Promise((resolve, reject) => { + let lock = false; + const handler = async (_: number): Promise => { + if (now() - submitStart > this.submitTimeout) return reject("timeout"); + if (lock) return; + lock = true; + + const block = await relayer.provider.getBlock("latest"); + const params = [ + signedRawTx, + { + knownAccounts: storageMap, + blockNumberMin: block.number, + blockNumberMax: block.number + 180, // ~10 minutes + timestampMin: block.timestamp, + timestampMax: block.timestamp + 420, // 15 minutes + }, + ]; + + this.logger.debug({ + method, + ...transaction, + params, + }); + + this.logger.debug("Fastlane: Trying to submit..."); + + try { + const hash = await provider.send(method, params); + this.logger.debug(`Fastlane: Sent new bundle ${hash}`); + this.provider.removeListener("block", handler); + return resolve(hash); + } catch (err: any) { + if ( + !err || + !err.body || + !err.body.match(/is not participating in FastLane protocol/) + ) { + // some other error happened + this.provider.removeListener("block", handler); + return reject(err); + } + this.logger.debug( + "Fastlane: Validator is not participating in FastLane protocol. Trying again..." + ); + } finally { + lock = false; + } + }; + this.provider.on("block", handler); + }); + } +} diff --git a/packages/executor/src/services/BundlingService/relayers/flashbots.ts b/packages/executor/src/services/BundlingService/relayers/flashbots.ts index 309c878e..df8a2086 100644 --- a/packages/executor/src/services/BundlingService/relayers/flashbots.ts +++ b/packages/executor/src/services/BundlingService/relayers/flashbots.ts @@ -5,7 +5,6 @@ import { FlashbotsBundleProvider, FlashbotsBundleResolution, } from "@flashbots/ethers-provider-bundle"; -import { MempoolEntryStatus } from "@skandha/types/lib/executor"; import { Config } from "../../../config"; import { Bundle, NetworkConfig } from "../../../interfaces"; import { MempoolService } from "../../MempoolService"; @@ -13,6 +12,7 @@ import { ReputationService } from "../../ReputationService"; import { estimateBundleGasLimit } from "../utils"; import { Relayer } from "../interfaces"; import { now } from "../../../utils"; +import { ExecutorEventBus } from "../../SubscriptionService"; import { EntryPointService } from "../../EntryPointService"; import { BaseRelayer } from "./base"; @@ -28,6 +28,7 @@ export class FlashbotsRelayer extends BaseRelayer { entryPointService: EntryPointService, mempoolService: MempoolService, reputationService: ReputationService, + eventBus: ExecutorEventBus, metrics: PerChainMetrics | null ) { super( @@ -39,8 +40,14 @@ export class FlashbotsRelayer extends BaseRelayer { entryPointService, mempoolService, reputationService, + eventBus, metrics ); + if (!this.networkConfig.rpcEndpointSubmit) { + throw Error( + "If you want to use Flashbots Builder API, please set API url in 'rpcEndpointSubmit' in config file" + ); + } } async sendBundle(bundle: Bundle): Promise { @@ -56,7 +63,6 @@ export class FlashbotsRelayer extends BaseRelayer { await mutex.runExclusive(async (): Promise => { const beneficiary = await this.selectBeneficiary(relayer); const entryPoint = entries[0]!.entryPoint; - const txRequest = this.entryPointService.encodeHandleOps( entryPoint, entries.map((entry) => entry.userOp), @@ -71,18 +77,14 @@ export class FlashbotsRelayer extends BaseRelayer { maxFeePerGas: bundle.maxFeePerGas, gasLimit: estimateBundleGasLimit( this.networkConfig.bundleGasLimitMarkup, - bundle.entries + bundle.entries, + this.networkConfig.estimationGasLimit ), 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); + if (!(await this.validateBundle(relayer, entries, transactionRequest))) { return; } @@ -94,21 +96,17 @@ export class FlashbotsRelayer extends BaseRelayer { (entry) => entry.userOpHash )}` ); - await this.mempoolService.setStatus( - entries, - MempoolEntryStatus.Submitted, - txHash - ); - await this.waitForTransaction(txHash).catch((err) => + await this.setSubmitted(entries, txHash); + await this.waitForEntries(entries).catch((err) => this.logger.error(err, "Flashbots: Could not find transaction") ); - await this.mempoolService.removeAll(entries); this.reportSubmittedUserops(txHash, bundle); }) .catch(async (err: any) => { + this.reportFailedBundle(); // 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.setNew(entries); if (err === "timeout") { this.logger.debug("Flashbots: Timeout"); return; diff --git a/packages/executor/src/services/BundlingService/relayers/index.ts b/packages/executor/src/services/BundlingService/relayers/index.ts index 15414380..d3910dee 100644 --- a/packages/executor/src/services/BundlingService/relayers/index.ts +++ b/packages/executor/src/services/BundlingService/relayers/index.ts @@ -1,2 +1,21 @@ +import { ClassicRelayer } from "./classic"; +import { FlashbotsRelayer } from "./flashbots"; +import { MerkleRelayer } from "./merkle"; +import { KolibriRelayer } from "./kolibri"; +import { EchoRelayer } from "./echo"; +import { FastlaneRelayer } from "./fastlane"; + export * from "./classic"; export * from "./flashbots"; +export * from "./merkle"; +export * from "./kolibri"; +export * from "./echo"; +export * from "./fastlane"; + +export type RelayerClass = + | typeof ClassicRelayer + | typeof FlashbotsRelayer + | typeof MerkleRelayer + | typeof KolibriRelayer + | typeof EchoRelayer + | typeof FastlaneRelayer; diff --git a/packages/executor/src/services/BundlingService/relayers/kolibri.ts b/packages/executor/src/services/BundlingService/relayers/kolibri.ts new file mode 100644 index 00000000..a4ac6370 --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/kolibri.ts @@ -0,0 +1,191 @@ +import { providers } from "ethers"; +import { PerChainMetrics } from "@skandha/monitoring/lib"; +import { Logger } from "@skandha/types/lib"; +import { fetchJson } from "ethers/lib/utils"; +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 { ExecutorEventBus } from "../../SubscriptionService"; +import { EntryPointService } from "../../EntryPointService"; +import { BaseRelayer } from "./base"; + +export class KolibriRelayer extends BaseRelayer { + constructor( + logger: Logger, + chainId: number, + provider: providers.JsonRpcProvider, + config: Config, + networkConfig: NetworkConfig, + entryPointService: EntryPointService, + mempoolService: MempoolService, + reputationService: ReputationService, + eventBus: ExecutorEventBus, + metrics: PerChainMetrics | null + ) { + super( + logger, + chainId, + provider, + config, + networkConfig, + entryPointService, + mempoolService, + reputationService, + eventBus, + metrics + ); + } + + async sendBundle(bundle: Bundle): 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 () => { + const beneficiary = await this.selectBeneficiary(relayer); + const entryPoint = entries[0]!.entryPoint; + const txRequest = this.entryPointService.encodeHandleOps( + entryPoint, + 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, + this.networkConfig.estimationGasLimit + ), + chainId: this.provider._network.chainId, + nonce: await relayer.getTransactionCount(), + }; + + if (!(await this.validateBundle(relayer, entries, transactionRequest))) { + return; + } + + this.logger.debug(transactionRequest, "Kolibri: Submitting"); + await this.submitTransaction(relayer, transactionRequest) + .then(async (hash: string) => { + this.logger.debug(`Bundle submitted: ${hash}`); + this.logger.debug( + `User op hashes ${entries.map((entry) => entry.userOpHash)}` + ); + await this.setSubmitted(entries, hash); + await this.waitForEntries(entries).catch((err) => + this.logger.error(err, "Kolibri: Could not find transaction") + ); + }) + .catch(async (err) => { + this.reportFailedBundle(); + await this.setNew(entries); + await this.handleUserOpFail(entries, err); + }); + }); + } + + private async submitTransaction( + relayer: Relayer, + transaction: providers.TransactionRequest + ): Promise { + const signedRawTx = await relayer.signTransaction(transaction); + const kolibriProvider = new KolibriJsonRpcProvider( + this.networkConfig.rpcEndpointSubmit + ); + + // refer to Kolibri docs - https://docs.kolibr.io/ + const params = { + tx_raw_data: signedRawTx, + broadcaster_address: await relayer.getAddress(), + ofa_config: { + enabled: true, + allow_front_run: false, + }, + submit_config: { + allow_reverts: false, + public_fallback: false, + mode: "private", + }, + }; + this.logger.debug(params, "Kolibri: request params"); + + return await kolibriProvider + .send("check_and_submit_bev", params) + .then((result: KolibriSuccessResponse) => { + this.logger.debug(result, "Kolibri: submit succeed"); + if (result.tx_hash) { + return result.tx_hash; + } + throw new Error("Could not submit transaction"); + }) + .catch((error: KolibriErrorResponse) => { + this.logger.error(error, "Kobliri: submit failed"); + throw error; + }); + } +} + +export class KolibriJsonRpcProvider extends providers.JsonRpcProvider { + send( + method: string, + params: Record, + authKey?: string + ): Promise { + if (authKey != undefined) { + if (!this.connection.headers) { + this.connection.headers = {}; + } + this.connection.headers["authorization"] = authKey; + } + + // the rest is the copy of JsonRpcProvider.send() + const request = { + method: method, + params: params, + id: this._nextId++, + jsonrpc: "2.0", + }; + + return fetchJson( + this.connection, + JSON.stringify(request), + (payload: { + error?: KolibriErrorResponse; + result?: KolibriSuccessResponse; + }): KolibriSuccessResponse | undefined => { + if (payload.error) { + const error: any = new Error(payload.error.message); + error.code = payload.error.code; + error.data = payload.error.data; + throw error as KolibriErrorResponse; + } + return payload.result; + } + ); + } +} + +type KolibriSuccessResponse = { + status_code: number; + tx_hash: string; + recommended_time_to_wait_ms: number; +}; + +type KolibriErrorResponse = { + code: number; + message: string; + data: string; +}; diff --git a/packages/executor/src/services/BundlingService/relayers/merkle.ts b/packages/executor/src/services/BundlingService/relayers/merkle.ts new file mode 100644 index 00000000..70624458 --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/merkle.ts @@ -0,0 +1,193 @@ +import path from "node:path"; +import { providers } from "ethers"; +import { PerChainMetrics } from "@skandha/monitoring/lib"; +import { Logger } from "@skandha/types/lib"; +import { AccessList, fetchJson } from "ethers/lib/utils"; +import { Config } from "../../../config"; +import { Bundle, NetworkConfig } from "../../../interfaces"; +import { MempoolService } from "../../MempoolService"; +import { ReputationService } from "../../ReputationService"; +import { estimateBundleGasLimit } from "../utils"; +import { now } from "../../../utils"; +import { ExecutorEventBus } from "../../SubscriptionService"; +import { EntryPointService } from "../../EntryPointService"; +import { BaseRelayer } from "./base"; + +export class MerkleRelayer extends BaseRelayer { + private submitTimeout = 2 * 60 * 1000; // 2 minutes + + constructor( + logger: Logger, + chainId: number, + provider: providers.JsonRpcProvider, + config: Config, + networkConfig: NetworkConfig, + entryPointService: EntryPointService, + mempoolService: MempoolService, + reputationService: ReputationService, + eventBus: ExecutorEventBus, + metrics: PerChainMetrics | null + ) { + super( + logger, + chainId, + provider, + config, + networkConfig, + entryPointService, + mempoolService, + reputationService, + eventBus, + metrics + ); + if ( + !this.networkConfig.rpcEndpointSubmit || + !this.networkConfig.merkleApiURL + ) { + throw Error( + "If you want to use Merkle API, please set RPC url in 'rpcEndpointSubmit' and API url in `merkleApiURL` in config file" + ); + } + } + + async sendBundle(bundle: Bundle): 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 () => { + const beneficiary = await this.selectBeneficiary(relayer); + const entryPoint = entries[0]!.entryPoint; + const txRequest = this.entryPointService.encodeHandleOps( + entryPoint, + 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, + this.networkConfig.estimationGasLimit + ), + chainId: this.provider._network.chainId, + nonce: await relayer.getTransactionCount(), + }; + + 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 (!(await this.validateBundle(relayer, entries, transactionRequest))) { + return; + } + + this.logger.debug(transactionRequest, "Merkle: Submitting"); + const merkleProvider = new providers.JsonRpcProvider( + this.networkConfig.rpcEndpointSubmit + ); + const signedRawTx = await relayer.signTransaction(transactionRequest); + const params = !this.networkConfig.conditionalTransactions + ? [signedRawTx] + : [signedRawTx, { knownAccounts: storageMap }]; + try { + const hash = await merkleProvider.send( + "eth_sendRawTransaction", + params + ); + this.logger.debug(`Bundle submitted: ${hash}`); + this.logger.debug( + `User op hashes ${entries.map((entry) => entry.userOpHash)}` + ); + await this.setSubmitted(entries, hash); + await this.waitForTransaction(hash); + } catch (err) { + this.reportFailedBundle(); + await this.setNew(entries); + await this.handleUserOpFail(entries, err); + } + }); + } + + async waitForTransaction(hash: string): Promise { + const txStatusUrl = new URL( + path.join("transaction", hash), + this.networkConfig.merkleApiURL + ).toString(); + const submitStart = now(); + return new Promise((resolve, reject) => { + let lock = false; + const handler = async (): Promise => { + this.logger.debug("Merkle: Fetching tx status"); + if (now() - submitStart > this.submitTimeout) return reject("timeout"); + if (lock) return; + lock = true; + try { + // https://docs.merkle.io/private-pool/wallets/transaction-status + const status = await fetchJson(txStatusUrl); + this.logger.debug(status, `Merkle: ${hash}`); + switch (status.status) { + case "nonce_too_low": + case "not_enough_funds": + case "base_fee_low": + case "low_priority_fee": + case "not_enough_gas": + case "sanctioned": + case "gas_limit_too_high": + case "invalid_signature": + case "nonce_gapped": + reject("rebundle"); // the bundle can be submitted again, no need to delete userops + break; + default: { + const response = await this.provider.getTransaction(hash); + if (response == null) { + this.logger.debug( + "Transaction not found yet. Trying again in 2 seconds" + ); + setTimeout(() => handler(), 2000); // fetch status again in 2 seconds + lock = false; + return; + } + this.logger.debug("Transaction is found"); + resolve(true); // transaction is found + } + } + } catch (err: any) { + this.logger.debug(err, "Could not fetch transaction status"); + // transaction is not found, but not necessarily failed + if (err.status === 400) { + setTimeout(() => handler(), 2000); // fetch status again in 2 seconds + lock = false; + return; + } + reject(err); + } + }; + void handler(); + }); + } +} diff --git a/packages/executor/src/services/BundlingService/service.ts b/packages/executor/src/services/BundlingService/service.ts index a238041e..28583fb0 100644 --- a/packages/executor/src/services/BundlingService/service.ts +++ b/packages/executor/src/services/BundlingService/service.ts @@ -7,7 +7,11 @@ import { RelayingMode, ReputationStatus, } from "@skandha/types/lib/executor"; -import { GasPriceMarkupOne, chainsWithoutEIP1559, getGasFee } from "@skandha/params/lib"; +import { + GasPriceMarkupOne, + chainsWithoutEIP1559, + getGasFee, +} from "@skandha/params/lib"; import { IGetGasFeeResult } from "@skandha/params/lib/gas-price-oracles/oracles"; import { Mutex } from "async-mutex"; import { Config } from "../../config"; @@ -22,9 +26,19 @@ import { UserOpValidationService } from "../UserOpValidation"; import { mergeStorageMap } from "../../utils/mergeStorageMap"; import { wait } from "../../utils"; import { MempoolEntry } from "../../entities/MempoolEntry"; +import { ExecutorEventBus } from "../SubscriptionService"; import { EntryPointService } from "../EntryPointService"; import { IRelayingMode } from "./interfaces"; -import { ClassicRelayer, FlashbotsRelayer } from "./relayers"; +import { + ClassicRelayer, + FlashbotsRelayer, + MerkleRelayer, + RelayerClass, + KolibriRelayer, + EchoRelayer, + FastlaneRelayer, +} from "./relayers"; +import { getUserOpGasLimit } from "./utils"; export class BundlingService { private mutex: Mutex; @@ -43,6 +57,7 @@ export class BundlingService { private mempoolService: MempoolService, private userOpValidationService: UserOpValidationService, private reputationService: ReputationService, + private eventBus: ExecutorEventBus, private config: Config, private logger: Logger, private metrics: PerChainMetrics | null, @@ -51,32 +66,40 @@ export class BundlingService { this.mutex = new Mutex(); this.networkConfig = config.getNetworkConfig(); + let Relayer: RelayerClass; + if (relayingMode === "flashbots") { this.logger.debug("Using flashbots relayer"); - this.relayer = new FlashbotsRelayer( - this.logger, - this.chainId, - this.provider, - this.config, - this.networkConfig, - this.entryPointService, - this.mempoolService, - this.reputationService, - this.metrics - ); + Relayer = FlashbotsRelayer; + } else if (relayingMode === "merkle") { + this.logger.debug("Using merkle relayer"); + Relayer = MerkleRelayer; + } else if (relayingMode === "kolibri") { + this.logger.debug("Using kolibri relayer"); + Relayer = KolibriRelayer; + } else if (relayingMode === "echo") { + this.logger.debug("Using echo relayer"); + Relayer = EchoRelayer; + } else if (relayingMode === "fastlane") { + this.logger.debug("Using fastlane relayer"); + Relayer = FastlaneRelayer; + this.maxSubmitAttempts = 5; } else { - this.relayer = new ClassicRelayer( - this.logger, - this.chainId, - this.provider, - this.config, - this.networkConfig, - this.entryPointService, - this.mempoolService, - this.reputationService, - this.metrics - ); + this.logger.debug("Using classic relayer"); + Relayer = ClassicRelayer; } + this.relayer = new Relayer( + this.logger, + this.chainId, + this.provider, + this.config, + this.networkConfig, + this.entryPointService, + this.mempoolService, + this.reputationService, + this.eventBus, + this.metrics + ); this.bundlingMode = "auto"; this.autoBundlingInterval = this.networkConfig.bundleInterval; @@ -105,8 +128,6 @@ export class BundlingService { gasFee: IGetGasFeeResult, entries: MempoolEntry[] ): Promise { - // TODO: support multiple entry points - // filter bundles by entry points const bundle: Bundle = { storageMap: {}, entries: [], @@ -114,6 +135,7 @@ export class BundlingService { maxPriorityFeePerGas: BigNumber.from(0), }; + const gasLimit = BigNumber.from(0); const paymasterDeposit: { [key: string]: BigNumber } = {}; const stakedEntityCount: { [key: string]: number } = {}; const senders = new Set(); @@ -122,11 +144,23 @@ export class BundlingService { }); for (const entry of entries) { + if ( + getUserOpGasLimit(entry.userOp, gasLimit).gt( + this.networkConfig.bundleGasLimit + ) + ) { + this.logger.debug(`${entry.userOpHash} reached bundle gas limit`); + continue; + } // validate gas prices if enabled if (this.networkConfig.enforceGasPrice) { let { maxPriorityFeePerGas, maxFeePerGas } = gasFee; const { enforceGasPriceThreshold } = this.networkConfig; - if (chainsWithoutEIP1559.some((chainId) => chainId === this.chainId)) { + if ( + chainsWithoutEIP1559.some( + (chainId: number) => chainId === this.chainId + ) + ) { maxFeePerGas = maxPriorityFeePerGas = gasFee.gasPrice; } // userop max fee per gas = userop.maxFee * (100 + threshold) / 100; @@ -166,7 +200,14 @@ export class BundlingService { if (!entity) continue; const status = await this.reputationService.getStatus(entity); if (status === ReputationStatus.BANNED) { - await this.mempoolService.remove(entry); + this.logger.debug( + `${title} - ${entity} is banned. Deleting userop ${entry.userOpHash}...` + ); + await this.mempoolService.updateStatus( + entries, + MempoolEntryStatus.Cancelled, + { revertReason: `${title} - ${entity} is banned.` } + ); continue; } else if ( status === ReputationStatus.THROTTLED || @@ -201,8 +242,14 @@ export class BundlingService { entry.hash ); } catch (e: any) { - this.logger.debug(`failed 2nd validation: ${e.message}`); - await this.mempoolService.remove(entry); + this.logger.debug( + `${entry.userOpHash} failed 2nd validation: ${e.message}. Deleting...` + ); + await this.mempoolService.updateStatus( + entries, + MempoolEntryStatus.Cancelled, + { revertReason: e.message } + ); continue; } @@ -222,7 +269,6 @@ export class BundlingService { } } - // TODO: add total gas cap if (entities.paymaster) { const { paymaster } = entities; // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions @@ -235,6 +281,9 @@ export class BundlingService { if ( paymasterDeposit[paymaster]?.lt(validationResult.returnInfo.prefund) ) { + this.logger.debug( + `not enough balance in paymaster to pay for all UserOps: ${entry.userOpHash}` + ); // not enough balance in paymaster to pay for all UserOps // (but it passed validation, so it can sponsor them separately continue; @@ -323,57 +372,88 @@ export class BundlingService { async sendNextBundle(): Promise { await this.mutex.runExclusive(async () => { - let entries = await this.mempoolService.getNewEntriesSorted( - this.maxBundleSize - ); - if (!entries.length) return; - if (this.relayer.isLocked()) { - this.logger.debug("Have userops, but all relayers are busy."); + if (!(await this.relayer.canSubmitBundle())) { + this.logger.debug("Relayer: Can not submit bundle yet"); 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( + let relayersCount = this.relayer.getAvailableRelayersCount(); + if (relayersCount == 0) { + this.logger.debug("Relayers are busy"); + } + while (relayersCount-- > 0) { + let entries = await this.mempoolService.getNewEntriesSorted( this.maxBundleSize ); - } - 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).catch((err) => { - this.logger.error(err); - }); - this.logger.debug("Sent new bundle to Skandha relayer..."); + if (!entries.length) { + this.logger.debug("No new entries"); + return; + } + // remove entries from mempool if submitAttempts are greater than maxAttempts + const invalidEntries = entries.filter( + (entry) => entry.submitAttempts > this.maxSubmitAttempts + ); + if (invalidEntries.length > 0) { + this.logger.debug( + `Found ${invalidEntries.length} that reached max submit attempts, deleting them...` + ); + this.logger.debug( + invalidEntries.map((entry) => entry.userOpHash).join("; ") + ); + await this.mempoolService.updateStatus( + invalidEntries, + MempoolEntryStatus.Cancelled, + { + revertReason: + "Attempted to submit userop multiple times, but failed...", + } + ); + entries = await this.mempoolService.getNewEntriesSorted( + this.maxBundleSize + ); + } + if (!entries.length) { + this.logger.debug("No entries left"); + 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; + } + this.logger.debug("Found some entries, trying to create a bundle"); + const bundle = await this.createBundle(gasFee, entries); + if (!bundle.entries.length) return; + await this.mempoolService.updateStatus( + bundle.entries, + MempoolEntryStatus.Pending + ); + await this.mempoolService.attemptToBundle(bundle.entries); - // during testing against spec-tests we need to wait the block to be submitted - if (this.config.testingMode) { - await wait(500); + if (this.config.testingMode) { + // need to wait for the tx hash during testing + await this.relayer.sendBundle(bundle).catch((err) => { + this.logger.error(err); + }); + } else { + void this.relayer.sendBundle(bundle).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); + } } }); } diff --git a/packages/executor/src/services/BundlingService/utils/estimateBundleGasLimit.ts b/packages/executor/src/services/BundlingService/utils/estimateBundleGasLimit.ts index 1f74c99f..1a6d06b3 100644 --- a/packages/executor/src/services/BundlingService/utils/estimateBundleGasLimit.ts +++ b/packages/executor/src/services/BundlingService/utils/estimateBundleGasLimit.ts @@ -1,23 +1,46 @@ -import { BigNumber } from "ethers"; +import { BigNumber, BigNumberish } from "ethers"; +import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; import { MempoolEntry } from "../../../entities/MempoolEntry"; export function estimateBundleGasLimit( markup: number, - bundle: MempoolEntry[] + bundle: MempoolEntry[], + estimationGasLimit: BigNumberish = 0 ): 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); + gasLimit = getUserOpGasLimit(userOp, gasLimit); } if (gasLimit.lt(1e5)) { // gasLimit should at least be 1e5 to pass test in test-executor gasLimit = BigNumber.from(1e5); } - return gasLimit; + return gasLimit.gt(estimationGasLimit) + ? gasLimit + : BigNumber.from(estimationGasLimit); +} + +export function getUserOpGasLimit( + userOp: UserOperation, + markup: BigNumber = BigNumber.from(0), + estimationGasLimit: BigNumberish = 0 +): BigNumber { + const scwGasLimit = BigNumber.from(userOp.verificationGasLimit) + .mul(3) + .add(200000) // instead of PVG + .add(userOp.callGasLimit) + .mul(11) + .div(10) + .add(markup); + const pmGasLimit = + userOp.paymasterVerificationGasLimit == null + ? BigNumber.from(0) + : BigNumber.from(userOp.paymasterVerificationGasLimit).add( + userOp.paymasterPostOpGasLimit ?? 0 + ); + const gasLimit = scwGasLimit.add(pmGasLimit); + + return gasLimit.gt(estimationGasLimit) + ? gasLimit + : BigNumber.from(estimationGasLimit); } diff --git a/packages/executor/src/services/EntryPointService/service.ts b/packages/executor/src/services/EntryPointService/service.ts index 6c828195..5a5c2186 100644 --- a/packages/executor/src/services/EntryPointService/service.ts +++ b/packages/executor/src/services/EntryPointService/service.ts @@ -6,31 +6,21 @@ import { UserOperationByHashResponse, UserOperationReceipt, } from "@skandha/types/lib/api/interfaces"; -import { EntryPoint as EntryPointV7Contract } from "@skandha/types/lib/contracts/EPv7/core/EntryPoint"; import RpcError from "@skandha/types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; import { NetworkConfig, UserOpValidationResult } from "../../interfaces"; -import { ReputationService } from "../ReputationService"; import { EntryPointV7Service, IEntryPointService } from "./versions"; import { EntryPointVersion } from "./interfaces"; -import { - EntryPointV7EventsService, - IEntryPointEventsService, -} from "./eventListeners"; export class EntryPointService { private entryPoints: { [address: string]: IEntryPointService; } = {}; - private eventsService: { - [address: string]: IEntryPointEventsService; - } = {}; constructor( private chainId: number, private networkConfig: NetworkConfig, private provider: providers.JsonRpcProvider, - private reputationService: ReputationService, private db: IDbController, private logger: Logger ) { @@ -42,14 +32,6 @@ export class EntryPointService { this.provider, this.logger ); - this.eventsService[address] = new EntryPointV7EventsService( - addr, - this.chainId, - this.entryPoints[address].contract as EntryPointV7Contract, - this.reputationService, - this.db - ); - this.eventsService[address].initEventListener(); } } diff --git a/packages/executor/src/services/EntryPointService/versions/0.0.7.ts b/packages/executor/src/services/EntryPointService/versions/0.0.7.ts index 638cc790..71da024e 100644 --- a/packages/executor/src/services/EntryPointService/versions/0.0.7.ts +++ b/packages/executor/src/services/EntryPointService/versions/0.0.7.ts @@ -5,7 +5,7 @@ import { import { _deployedBytecode } from "@skandha/types/lib/contracts/EPv7/factories/core/EntryPointSimulations__factory"; import { IStakeManager } from "@skandha/types/lib/contracts/EPv7/core/EntryPointSimulations"; import { EntryPoint__factory } from "@skandha/types/lib/contracts/EPv7/factories/core"; -import { BigNumber, providers } from "ethers"; +import { BigNumber, constants, providers } from "ethers"; import RpcError from "@skandha/types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; import { @@ -22,6 +22,7 @@ import { UserOperationReceipt, UserOperationByHashResponse, } from "@skandha/types/lib/api/interfaces"; +import { deepHexlify } from "@skandha/utils/lib/hexlify"; import { encodeUserOp, mergeValidationDataValues, @@ -34,10 +35,10 @@ import { StakeInfo, UserOpValidationResult, } from "../../../interfaces"; -import { deepHexlify } from "../../../utils"; import { DefaultGasOverheads } from "../constants"; import { StateOverrides } from "../interfaces"; import { decodeRevertReason } from "../utils/decodeRevertReason"; +import { getUserOpGasLimit } from "../../BundlingService/utils"; import { IEntryPointService } from "./base"; const entryPointSimulations = IEntryPointSimulations__factory.createInterface(); @@ -62,15 +63,26 @@ export class EntryPointV7Service implements IEntryPointService { } async simulateHandleOp(userOp: UserOperation): Promise { + const gasLimit = this.networkConfig.gasFeeInSimulation + ? getUserOpGasLimit( + userOp, + constants.Zero, + this.networkConfig.estimationGasLimit + ) + : undefined; + const [data, stateOverrides] = this.encodeSimulateHandleOp( userOp, AddressZero, BytesZero ); + const tx: providers.TransactionRequest = { to: this.address, data, + gasLimit, }; + try { const simulationResult = await this.provider.send("eth_call", [ tx, diff --git a/packages/executor/src/services/EventsService/index.ts b/packages/executor/src/services/EventsService/index.ts new file mode 100644 index 00000000..98d5784d --- /dev/null +++ b/packages/executor/src/services/EventsService/index.ts @@ -0,0 +1,43 @@ +import { EntryPoint as IEntryPointV7 } from "@skandha/types/lib/contracts/EPv7/core/EntryPoint"; +import { IDbController, Logger } from "@skandha/types/lib"; +import { ReputationService } from "../ReputationService"; +import { MempoolService } from "../MempoolService"; +import { EntryPointService } from "../EntryPointService"; +import { NetworkConfig } from "../../interfaces"; +import { ExecutorEventBus } from "../SubscriptionService"; +import { + EntryPointV7EventsService, + IEntryPointEventsService, +} from "./versions"; + +export class EventsService { + private eventsService: { + [address: string]: IEntryPointEventsService; + } = {}; + + constructor( + private chainId: number, + private networkConfig: NetworkConfig, + private reputationService: ReputationService, + private mempoolService: MempoolService, + private entryPointService: EntryPointService, + private eventBus: ExecutorEventBus, + private db: IDbController, + private logger: Logger + ) { + for (const addr of this.networkConfig.entryPoints) { + const address = addr.toLowerCase(); + this.eventsService[address] = new EntryPointV7EventsService( + addr, + this.chainId, + this.entryPointService.getEntryPoint(address).contract as IEntryPointV7, + this.reputationService, + this.mempoolService, + this.eventBus, + this.db, + this.logger + ); + this.eventsService[address].initEventListener(); + } + } +} diff --git a/packages/executor/src/services/EntryPointService/eventListeners/0.0.7.ts b/packages/executor/src/services/EventsService/versions/0.0.7.ts similarity index 71% rename from packages/executor/src/services/EntryPointService/eventListeners/0.0.7.ts rename to packages/executor/src/services/EventsService/versions/0.0.7.ts index 7b157893..1d1b3de7 100644 --- a/packages/executor/src/services/EntryPointService/eventListeners/0.0.7.ts +++ b/packages/executor/src/services/EventsService/versions/0.0.7.ts @@ -1,12 +1,15 @@ -import { IDbController } from "@skandha/types/lib"; +import { IDbController, Logger } from "@skandha/types/lib"; import { AccountDeployedEvent, SignatureAggregatorChangedEvent, UserOperationEventEvent, EntryPoint, } from "@skandha/types/lib/contracts/EPv7/core/EntryPoint"; -import { TypedEvent } from "@skandha/types/lib/contracts/common"; +import { MempoolEntryStatus } from "@skandha/types/lib/executor"; +import { TypedEvent, TypedListener } from "@skandha/types/lib/contracts/common"; import { ReputationService } from "../../ReputationService"; +import { MempoolService } from "../../MempoolService"; +import { ExecutorEvent, ExecutorEventBus } from "../../SubscriptionService"; export class EntryPointV7EventsService { private lastBlock = 0; @@ -17,7 +20,10 @@ export class EntryPointV7EventsService { private chainId: number, private contract: EntryPoint, private reputationService: ReputationService, - private db: IDbController + private mempoolService: MempoolService, + private eventBus: ExecutorEventBus, + private db: IDbController, + private logger: Logger ) { this.LAST_BLOCK_KEY = `${this.chainId}:LAST_PARSED_BLOCK:${this.entryPoint}`; } @@ -27,11 +33,21 @@ export class EntryPointV7EventsService { this.contract.filters.UserOperationEvent(), async (...args) => { const ev = args[args.length - 1]; - await this.handleEvent(ev as any); + await this.handleEvent(ev as ParsedEventType); } ); } + onUserOperationEvent(callback: TypedListener): void { + this.contract.on(this.contract.filters.UserOperationEvent(), callback); + } + + offUserOperationEvent( + callback: TypedListener + ): void { + this.contract.off(this.contract.filters.UserOperationEvent(), callback); + } + /** * manually handle all new events since last run */ @@ -54,12 +70,7 @@ export class EntryPointV7EventsService { await this.saveLastBlockPerEntryPoints(); } - async handleEvent( - ev: - | UserOperationEventEvent - | AccountDeployedEvent - | SignatureAggregatorChangedEvent - ): Promise { + async handleEvent(ev: ParsedEventType): Promise { switch (ev.event) { case "UserOperationEvent": await this.handleUserOperationEvent(ev as UserOperationEventEvent); @@ -101,6 +112,18 @@ export class EntryPointV7EventsService { } async handleUserOperationEvent(ev: UserOperationEventEvent): Promise { + const entry = await this.mempoolService.getEntryByHash(ev.args.userOpHash); + if (entry) { + this.logger.debug( + `Found UserOperationEvent for ${ev.args.userOpHash}. Deleting userop...` + ); + await this.mempoolService.updateStatus( + [entry], + MempoolEntryStatus.OnChain, + { transaction: ev.transactionHash } + ); + this.eventBus.emit(ExecutorEvent.onChainUserOps, entry); + } await this.includedAddress(ev.args.sender); await this.includedAddress(ev.args.paymaster); await this.includedAddress(this.getEventAggregator(ev)); @@ -126,3 +149,8 @@ export class EntryPointV7EventsService { } } } + +type ParsedEventType = + | UserOperationEventEvent + | AccountDeployedEvent + | SignatureAggregatorChangedEvent; diff --git a/packages/executor/src/services/EntryPointService/eventListeners/base.ts b/packages/executor/src/services/EventsService/versions/base.ts similarity index 100% rename from packages/executor/src/services/EntryPointService/eventListeners/base.ts rename to packages/executor/src/services/EventsService/versions/base.ts diff --git a/packages/executor/src/services/EntryPointService/eventListeners/index.ts b/packages/executor/src/services/EventsService/versions/index.ts similarity index 100% rename from packages/executor/src/services/EntryPointService/eventListeners/index.ts rename to packages/executor/src/services/EventsService/versions/index.ts diff --git a/packages/executor/src/services/MempoolService.ts b/packages/executor/src/services/MempoolService.ts deleted file mode 100644 index 34ae5c2c..00000000 --- a/packages/executor/src/services/MempoolService.ts +++ /dev/null @@ -1,419 +0,0 @@ -import { BigNumberish, utils } from "ethers"; -import { IDbController } from "@skandha/types/lib"; -import RpcError from "@skandha/types/lib/api/errors/rpc-error"; -import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; -import { - IEntityWithAggregator, - MempoolEntryStatus, - IWhitelistedEntities, - ReputationStatus, -} from "@skandha/types/lib/executor"; -import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; -import { getAddr, now } from "../utils"; -import { MempoolEntry } from "../entities/MempoolEntry"; -import { IMempoolEntry, MempoolEntrySerialized } from "../entities/interfaces"; -import { KnownEntities, NetworkConfig, StakeInfo } from "../interfaces"; -import { ReputationService } from "./ReputationService"; -import { EntryPointService } from "./EntryPointService"; - -export class MempoolService { - private MAX_MEMPOOL_USEROPS_PER_SENDER = 4; - private THROTTLED_ENTITY_MEMPOOL_COUNT = 4; - private USEROP_COLLECTION_KEY: string; - private USEROP_HASHES_COLLECTION_PREFIX: string; // stores userop all hashes, independent of a chain - - constructor( - private db: IDbController, - private chainId: number, - private entryPointService: EntryPointService, - private reputationService: ReputationService, - private networkConfig: NetworkConfig - ) { - this.USEROP_COLLECTION_KEY = `${chainId}:USEROPKEYS`; - this.USEROP_HASHES_COLLECTION_PREFIX = "USEROPHASH:"; - } - - async count(): Promise { - const userOpKeys: string[] = await this.fetchKeys(); - return userOpKeys.length; - } - - async dump(): Promise { - return (await this.fetchAll()).map((entry) => entry.serialize()); - } - - async addUserOp( - userOp: UserOperation, - entryPoint: string, - prefund: BigNumberish, - senderInfo: StakeInfo, - factoryInfo: StakeInfo | undefined, - paymasterInfo: StakeInfo | undefined, - aggregatorInfo: StakeInfo | undefined, - userOpHash: string, - hash?: string, - aggregator?: string - ): Promise { - const entry = new MempoolEntry({ - chainId: this.chainId, - userOp, - entryPoint, - prefund, - aggregator, - hash, - userOpHash, - factory: this.entryPointService.getFactory(entryPoint, userOp), - paymaster: this.entryPointService.getPaymaster(entryPoint, userOp), - }); - const existingEntry = await this.find(entry); - if (existingEntry) { - await this.validateReplaceability(entry, existingEntry); - await this.db.put(this.getKey(entry), { - ...entry, - lastUpdatedTime: now(), - }); - await this.removeUserOpHash(existingEntry.userOpHash); - await this.saveUserOpHash(entry.userOpHash, entry); - } else { - await this.checkEntityCountInMempool( - entry, - senderInfo, - factoryInfo, - paymasterInfo, - aggregatorInfo - ); - await this.checkMultipleRolesViolation(entry); - const userOpKeys = await this.fetchKeys(); - const key = this.getKey(entry); - userOpKeys.push(key); - await this.db.put(this.USEROP_COLLECTION_KEY, userOpKeys); - await this.db.put(key, { ...entry, lastUpdatedTime: now() }); - await this.saveUserOpHash(entry.userOpHash, entry); - } - await this.updateSeenStatus(entryPoint, 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; - } - const key = this.getKey(entry); - const newKeys = (await this.fetchKeys()).filter((k) => k !== key); - await this.db.del(key); - 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); - } - - async removeUserOpHash(hash: string): Promise { - await this.db.del(`${this.USEROP_HASHES_COLLECTION_PREFIX}${hash}`); - } - - async getEntryByHash(hash: string): Promise { - const key = await this.db - .get(`${this.USEROP_HASHES_COLLECTION_PREFIX}${hash}`) - .catch(() => null); - if (!key) return null; - return this.findByKey(key); - } - - async getNewEntriesSorted(size: number): Promise { - const allEntries = await this.fetchAll(); - return allEntries - .filter((entry) => entry.status === MempoolEntryStatus.New) - .sort(MempoolEntry.compareByCost) - .slice(0, size); - } - - async clearState(): Promise { - const keys = await this.fetchKeys(); - for (const key of keys) { - await this.db.del(key); - } - await this.db.del(this.USEROP_COLLECTION_KEY); - } - - async find(entry: MempoolEntry): Promise { - return this.findByKey(this.getKey(entry)); - } - - async findByKey(key: string): Promise { - const raw = await this.db.get(key).catch(() => null); - if (raw) { - return this.rawEntryToMempoolEntry(raw); - } - return null; - } - - async validateReplaceability( - newEntry: MempoolEntry, - oldEntry?: MempoolEntry | null - ): Promise { - if (!oldEntry) { - oldEntry = await this.find(newEntry); - } - if ( - !oldEntry || - newEntry.canReplaceWithTTL(oldEntry, this.networkConfig.useropsTTL) - ) { - return true; - } - throw new RpcError( - "User op cannot be replaced: fee too low", - RpcErrorCodes.INVALID_USEROP - ); - } - - async validateUserOpReplaceability( - userOp: UserOperation, - entryPoint: string - ): Promise { - const entry = new MempoolEntry({ - chainId: this.chainId, - userOp, - entryPoint, - prefund: "0", - userOpHash: "", - }); - return this.validateReplaceability(entry); - } - - getKey(entry: Pick): string { - const { userOp, chainId } = entry; - return `${chainId}:${userOp.sender.toLowerCase()}:${userOp.nonce}`; - } - - async fetchKeys(): Promise { - const userOpKeys = await this.db - .get(this.USEROP_COLLECTION_KEY) - .catch(() => []); - return userOpKeys; - } - - async fetchAll(): Promise { - const keys = await this.fetchKeys(); - const rawEntries = await this.db - .getMany(keys) - .catch(() => []); - return rawEntries.map(this.rawEntryToMempoolEntry); - } - - async fetchManyByKeys(keys: string[]): Promise { - const rawEntries = await this.db - .getMany(keys) - .catch(() => []); - return rawEntries.map(this.rawEntryToMempoolEntry); - } - - private async checkEntityCountInMempool( - entry: MempoolEntry, - accountInfo: StakeInfo, - factoryInfo: StakeInfo | undefined, - paymasterInfo: StakeInfo | undefined, - aggregatorInfo: StakeInfo | undefined - ): Promise { - const mEntries = await this.fetchAll(); - const titles: IEntityWithAggregator[] = [ - "account", - "factory", - "paymaster", - "aggregator", - ]; - const count = [1, 1, 1, 1]; // starting all values from one because `entry` param counts as well - const stakes = [accountInfo, factoryInfo, paymasterInfo, aggregatorInfo]; - for (const mEntry of mEntries) { - if ( - utils.getAddress(mEntry.userOp.sender) == - utils.getAddress(accountInfo.addr) - ) { - count[0]++; - } - // counts the number of similar factories, paymasters and aggregator in the mempool - for (let i = 1; i < 4; ++i) { - const mEntity = mEntry[titles[i] as keyof MempoolEntry] as string; - if ( - stakes[i] && - mEntity && - utils.getAddress(mEntity) == utils.getAddress(stakes[i]!.addr) - ) { - count[i]++; - } - } - } - - // check for ban - for (const [index, stake] of stakes.entries()) { - if (!stake) continue; - const whitelist = - this.networkConfig.whitelistedEntities[ - titles[index] as keyof IWhitelistedEntities - ]; - if ( - stake.addr && - whitelist != null && - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - whitelist.some( - (addr) => utils.getAddress(addr) === utils.getAddress(stake.addr) - ) - ) { - continue; - } - const status = await this.reputationService.getStatus(stake.addr); - if (status === ReputationStatus.BANNED) { - throw new RpcError( - `${titles[index]} ${stake.addr} is banned`, - RpcErrorCodes.PAYMASTER_OR_AGGREGATOR_BANNED - ); - } - if ( - status === ReputationStatus.THROTTLED && - count[index] > this.THROTTLED_ENTITY_MEMPOOL_COUNT - ) { - throw new RpcError( - `${titles[index]} ${stake.addr} is throttled`, - RpcErrorCodes.PAYMASTER_OR_AGGREGATOR_BANNED - ); - } - const reputationEntry = - index === 0 ? null : await this.reputationService.fetchOne(stake.addr); - const maxMempoolCount = - index === 0 - ? this.MAX_MEMPOOL_USEROPS_PER_SENDER - : this.reputationService.calculateMaxAllowedMempoolOpsUnstaked( - reputationEntry! - ); - if (count[index] > maxMempoolCount) { - const checkStake = await this.reputationService.checkStake(stake); - if (checkStake.code !== 0) { - throw new RpcError(checkStake.msg, checkStake.code); - } - } - } - } - - private async checkMultipleRolesViolation( - entry: MempoolEntry - ): Promise { - const { userOp, entryPoint } = entry; - const { otherEntities, accounts } = await this.getKnownEntities(); - if (otherEntities.includes(utils.getAddress(userOp.sender))) { - throw new RpcError( - `The sender address "${userOp.sender}" is used as a different entity in another UserOperation currently in mempool`, - RpcErrorCodes.INVALID_OPCODE - ); - } - - const paymaster = this.entryPointService.getPaymaster(entryPoint, userOp); - if (paymaster) { - if (accounts.includes(utils.getAddress(paymaster))) { - throw new RpcError( - `A Paymaster at ${paymaster} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.`, - RpcErrorCodes.INVALID_OPCODE - ); - } - } - - const factory = this.entryPointService.getFactory(entryPoint, userOp); - if (factory) { - if (accounts.includes(utils.getAddress(factory))) { - throw new RpcError( - `A Factory at ${factory} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.`, - RpcErrorCodes.INVALID_OPCODE - ); - } - } - } - - rawEntryToMempoolEntry(raw: IMempoolEntry): MempoolEntry { - return new MempoolEntry({ - chainId: raw.chainId, - userOp: raw.userOp, - entryPoint: raw.entryPoint, - prefund: raw.prefund, - aggregator: raw.aggregator, - factory: raw.factory, - paymaster: raw.paymaster, - hash: raw.hash, - userOpHash: raw.userOpHash, - lastUpdatedTime: raw.lastUpdatedTime, - transaction: raw.transaction, - status: raw.status, - submitAttempts: raw.submitAttempts, - }); - } - - /** - * returns a list of addresses of all entities in the mempool - */ - private async getKnownEntities(): Promise { - const entities: KnownEntities = { - accounts: [], - otherEntities: [], - }; - const entries = await this.fetchAll(); - for (const entry of entries) { - entities.accounts.push(utils.getAddress(entry.userOp.sender)); - if (entry.paymaster && entry.paymaster.length >= 42) { - entities.otherEntities.push( - utils.getAddress(getAddr(entry.paymaster)!) - ); - } - if (entry.factory && entry.factory.length >= 42) { - entities.otherEntities.push(utils.getAddress(getAddr(entry.factory)!)); - } - } - return entities; - } - - private async updateSeenStatus( - entryPoint: string, - userOp: UserOperation, - aggregator?: string - ): Promise { - const paymaster = this.entryPointService.getPaymaster(entryPoint, userOp); - const factory = this.entryPointService.getFactory(entryPoint, userOp); - await this.reputationService.updateSeenStatus(userOp.sender); - if (aggregator) { - await this.reputationService.updateSeenStatus(aggregator); - } - if (paymaster) { - await this.reputationService.updateSeenStatus(paymaster); - } - if (factory) { - await this.reputationService.updateSeenStatus(factory); - } - } -} diff --git a/packages/executor/src/services/MempoolService/constants.ts b/packages/executor/src/services/MempoolService/constants.ts new file mode 100644 index 00000000..9dd9af44 --- /dev/null +++ b/packages/executor/src/services/MempoolService/constants.ts @@ -0,0 +1,3 @@ +export const MAX_MEMPOOL_USEROPS_PER_SENDER = 4; +export const THROTTLED_ENTITY_MEMPOOL_COUNT = 4; +export const ARCHIVE_PURGE_INTERVAL = 5 * 60 * 1000; // 50 minutes diff --git a/packages/executor/src/services/MempoolService/index.ts b/packages/executor/src/services/MempoolService/index.ts new file mode 100644 index 00000000..6261f896 --- /dev/null +++ b/packages/executor/src/services/MempoolService/index.ts @@ -0,0 +1 @@ +export * from "./service"; diff --git a/packages/executor/src/services/MempoolService/reputation.ts b/packages/executor/src/services/MempoolService/reputation.ts new file mode 100644 index 00000000..5ce4145c --- /dev/null +++ b/packages/executor/src/services/MempoolService/reputation.ts @@ -0,0 +1,188 @@ +import { utils } from "ethers"; +import RpcError from "@skandha/types/lib/api/errors/rpc-error"; +import { + IEntityWithAggregator, + IWhitelistedEntities, + ReputationStatus, +} from "@skandha/types/lib/executor"; +import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; +import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; +import { MempoolEntry } from "../../entities/MempoolEntry"; +import { KnownEntities, NetworkConfig, StakeInfo } from "../../interfaces"; +import { ReputationService } from "../ReputationService"; +import { getAddr } from "../../utils"; +import { EntryPointService } from "../EntryPointService"; +import { MempoolService } from "./service"; +import { + MAX_MEMPOOL_USEROPS_PER_SENDER, + THROTTLED_ENTITY_MEMPOOL_COUNT, +} from "./constants"; + +export class MempoolReputationChecks { + constructor( + private service: MempoolService, + private entryPointService: EntryPointService, + private reputationService: ReputationService, + private networkConfig: NetworkConfig + ) {} + + async checkEntityCountInMempool( + entry: MempoolEntry, + accountInfo: StakeInfo, + factoryInfo: StakeInfo | undefined, + paymasterInfo: StakeInfo | undefined, + aggregatorInfo: StakeInfo | undefined + ): Promise { + const mEntries = await this.service.fetchPendingUserOps(); + const titles: IEntityWithAggregator[] = [ + "account", + "factory", + "paymaster", + "aggregator", + ]; + const count = [1, 1, 1, 1]; // starting all values from one because `entry` param counts as well + const stakes = [accountInfo, factoryInfo, paymasterInfo, aggregatorInfo]; + for (const mEntry of mEntries) { + if ( + utils.getAddress(mEntry.userOp.sender) == + utils.getAddress(accountInfo.addr) + ) { + count[0]++; + } + // counts the number of similar factories, paymasters and aggregator in the mempool + for (let i = 1; i < 4; ++i) { + const mEntity = mEntry[titles[i] as keyof MempoolEntry] as string; + if ( + stakes[i] && + mEntity && + utils.getAddress(mEntity) == utils.getAddress(stakes[i]!.addr) + ) { + count[i]++; + } + } + } + + // check for ban + for (const [index, stake] of stakes.entries()) { + if (!stake) continue; + const whitelist = + this.networkConfig.whitelistedEntities[ + titles[index] as keyof IWhitelistedEntities + ]; + if ( + stake.addr && + whitelist != null && + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + whitelist.some( + (addr: string) => + utils.getAddress(addr) === utils.getAddress(stake.addr) + ) + ) { + continue; + } + const status = await this.reputationService.getStatus(stake.addr); + if (status === ReputationStatus.BANNED) { + throw new RpcError( + `${titles[index]} ${stake.addr} is banned`, + RpcErrorCodes.PAYMASTER_OR_AGGREGATOR_BANNED + ); + } + if ( + status === ReputationStatus.THROTTLED && + count[index] > THROTTLED_ENTITY_MEMPOOL_COUNT + ) { + throw new RpcError( + `${titles[index]} ${stake.addr} is throttled`, + RpcErrorCodes.PAYMASTER_OR_AGGREGATOR_BANNED + ); + } + const reputationEntry = + index === 0 ? null : await this.reputationService.fetchOne(stake.addr); + const maxMempoolCount = + index === 0 + ? MAX_MEMPOOL_USEROPS_PER_SENDER + : this.reputationService.calculateMaxAllowedMempoolOpsUnstaked( + reputationEntry! + ); + if (count[index] > maxMempoolCount) { + const checkStake = await this.reputationService.checkStake(stake); + if (checkStake.code !== 0) { + throw new RpcError(checkStake.msg, checkStake.code); + } + } + } + } + + async checkMultipleRolesViolation(entry: MempoolEntry): Promise { + const { userOp, entryPoint } = entry; + const { otherEntities, accounts } = await this.getKnownEntities(); + if (otherEntities.includes(utils.getAddress(userOp.sender))) { + throw new RpcError( + `The sender address "${userOp.sender}" is used as a different entity in another UserOperation currently in mempool`, + RpcErrorCodes.INVALID_OPCODE + ); + } + + const paymaster = this.entryPointService.getPaymaster(entryPoint, userOp); + if (paymaster) { + if (accounts.includes(utils.getAddress(paymaster))) { + throw new RpcError( + `A Paymaster at ${paymaster} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.`, + RpcErrorCodes.INVALID_OPCODE + ); + } + } + + const factory = this.entryPointService.getFactory(entryPoint, userOp); + if (factory) { + if (accounts.includes(utils.getAddress(factory))) { + throw new RpcError( + `A Factory at ${factory} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.`, + RpcErrorCodes.INVALID_OPCODE + ); + } + } + } + + async updateSeenStatus( + entryPoint: string, + userOp: UserOperation, + aggregator?: string + ): Promise { + const paymaster = this.entryPointService.getPaymaster(entryPoint, userOp); + const factory = this.entryPointService.getFactory(entryPoint, userOp); + await this.reputationService.updateSeenStatus(userOp.sender); + if (aggregator) { + await this.reputationService.updateSeenStatus(aggregator); + } + if (paymaster) { + await this.reputationService.updateSeenStatus(paymaster); + } + if (factory) { + await this.reputationService.updateSeenStatus(factory); + } + } + + /** + * returns a list of addresses of all entities in the mempool + */ + private async getKnownEntities(): Promise { + const entities: KnownEntities = { + accounts: [], + otherEntities: [], + }; + const entries = await this.service.fetchPendingUserOps(); + for (const entry of entries) { + entities.accounts.push(utils.getAddress(entry.userOp.sender)); + if (entry.paymaster && entry.paymaster.length >= 42) { + entities.otherEntities.push( + utils.getAddress(getAddr(entry.paymaster)!) + ); + } + if (entry.factory && entry.factory.length >= 42) { + entities.otherEntities.push(utils.getAddress(getAddr(entry.factory)!)); + } + } + return entities; + } +} diff --git a/packages/executor/src/services/MempoolService/service.ts b/packages/executor/src/services/MempoolService/service.ts new file mode 100644 index 00000000..e960a24d --- /dev/null +++ b/packages/executor/src/services/MempoolService/service.ts @@ -0,0 +1,310 @@ +import { Mutex } from "async-mutex"; +import { IDbController, Logger } from "@skandha/types/lib"; +import { MempoolEntryStatus } from "@skandha/types/lib/executor"; +import RpcError from "@skandha/types/lib/api/errors/rpc-error"; +import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; +import { BigNumberish } from "ethers"; +import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; +import { ReputationService } from "../ReputationService"; +import { ExecutorEvent, ExecutorEventBus } from "../SubscriptionService"; +import { NetworkConfig, StakeInfo } from "../../interfaces"; +import { + IMempoolEntry, + MempoolEntrySerialized, +} from "../../entities/interfaces"; +import { MempoolEntry } from "../../entities/MempoolEntry"; +import { now } from "../../utils"; +import { EntryPointService } from "../EntryPointService"; +import { rawEntryToMempoolEntry } from "./utils"; +import { MempoolReputationChecks } from "./reputation"; +import { ARCHIVE_PURGE_INTERVAL } from "./constants"; + +export class MempoolService { + private USEROP_COLLECTION_KEY: string; + private USEROP_HASHES_COLLECTION_PREFIX: string; + private mutex = new Mutex(); + private reputationCheck: MempoolReputationChecks; + + constructor( + private db: IDbController, + private chainId: number, + private entryPointService: EntryPointService, + private reputationService: ReputationService, + private eventBus: ExecutorEventBus, + private networkConfig: NetworkConfig, + private logger: Logger + ) { + this.USEROP_COLLECTION_KEY = `${chainId}:USEROPKEYS`; + this.USEROP_HASHES_COLLECTION_PREFIX = "USEROPHASH:"; + this.reputationCheck = new MempoolReputationChecks( + this, + this.entryPointService, + this.reputationService, + this.networkConfig + ); + + setInterval(() => { + void this.deleteOldUserOps(); + }, ARCHIVE_PURGE_INTERVAL); // 5 minutes + } + + /** + * View functions + */ + + async dump(): Promise { + return (await this.fetchPendingUserOps()).map((entry) => entry.serialize()); + } + + async fetchPendingUserOps(): Promise { + return (await this.fetchAll()).filter( + (entry) => entry.status < MempoolEntryStatus.OnChain + ); + } + + async fetchManyByKeys(keys: string[]): Promise { + const rawEntries = await this.db + .getMany(keys) + .catch(() => []); + return rawEntries.map(rawEntryToMempoolEntry); + } + + async find(entry: MempoolEntry): Promise { + return this.findByKey(this.getKey(entry)); + } + + async getEntryByHash(hash: string): Promise { + const key = await this.db + .get(`${this.USEROP_HASHES_COLLECTION_PREFIX}${hash}`) + .catch(() => null); + if (!key) return null; + return this.findByKey(key); + } + + async getNewEntriesSorted(size: number, offset = 0): Promise { + const allEntries = await this.fetchAll(); + return allEntries + .filter((entry) => entry.status === MempoolEntryStatus.New) + .sort(MempoolEntry.compareByCost) + .slice(offset, offset + size); + } + + async validateUserOpReplaceability( + userOp: UserOperation, + entryPoint: string + ): Promise { + const entry = new MempoolEntry({ + chainId: this.chainId, + userOp, + entryPoint, + prefund: "0", + userOpHash: "", + }); + return this.validateReplaceability(entry); + } + + /** + * Write functions + */ + async updateStatus( + entries: MempoolEntry[], + status: MempoolEntryStatus, + params?: { + transaction?: string; + revertReason?: string; + } + ): Promise { + for (const entry of entries) { + entry.setStatus(status, params); + await this.update(entry); + + // event bus logic + if ( + [ + MempoolEntryStatus.Cancelled, + MempoolEntryStatus.Submitted, + MempoolEntryStatus.Reverted, + ].findIndex((st) => st === status) > -1 + ) { + this.eventBus.emit(ExecutorEvent.submittedUserOps, entry); + } + } + } + + async clearState(): Promise { + await this.mutex.runExclusive(async () => { + const keys = await this.fetchKeys(); + for (const key of keys) { + await this.db.del(key); + } + await this.db.del(this.USEROP_COLLECTION_KEY); + }); + } + + async attemptToBundle(entries: MempoolEntry[]): Promise { + for (const entry of entries) { + entry.submitAttempts++; + entry.lastUpdatedTime = now(); + await this.update(entry); + } + } + + async addUserOp( + userOp: UserOperation, + entryPoint: string, + prefund: BigNumberish, + senderInfo: StakeInfo, + factoryInfo: StakeInfo | undefined, + paymasterInfo: StakeInfo | undefined, + aggregatorInfo: StakeInfo | undefined, + userOpHash: string, + hash?: string, + aggregator?: string + ): Promise { + const entry = new MempoolEntry({ + chainId: this.chainId, + userOp, + entryPoint, + prefund, + aggregator, + hash, + userOpHash, + factory: this.entryPointService.getFactory(entryPoint, userOp), + paymaster: this.entryPointService.getPaymaster(entryPoint, userOp), + submittedTime: now(), + }); + await this.mutex.runExclusive(async () => { + const existingEntry = await this.find(entry); + if (existingEntry) { + await this.validateReplaceability(entry, existingEntry); + await this.db.put(this.getKey(entry), { + ...entry, + lastUpdatedTime: now(), + }); + await this.removeUserOpHash(existingEntry.userOpHash); + await this.saveUserOpHash(entry.userOpHash, entry); + this.logger.debug("Mempool: User op replaced"); + } else { + await this.reputationCheck.checkEntityCountInMempool( + entry, + senderInfo, + factoryInfo, + paymasterInfo, + aggregatorInfo + ); + await this.reputationCheck.checkMultipleRolesViolation(entry); + const userOpKeys = await this.fetchKeys(); + const key = this.getKey(entry); + userOpKeys.push(key); + await this.db.put(this.USEROP_COLLECTION_KEY, userOpKeys); + await this.db.put(key, { ...entry, lastUpdatedTime: now() }); + await this.saveUserOpHash(entry.userOpHash, entry); + this.logger.debug("Mempool: User op added"); + } + await this.reputationCheck.updateSeenStatus( + entryPoint, + userOp, + aggregator + ); + this.eventBus.emit(ExecutorEvent.pendingUserOps, entry); + }); + } + + async deleteOldUserOps(): Promise { + const removableEntries = (await this.fetchAll()).filter((entry) => { + if (entry.status < MempoolEntryStatus.OnChain) return false; + if ( + entry.lastUpdatedTime + this.networkConfig.archiveDuration * 1000 > + now() + ) { + return false; + } + return true; + }); + for (const entry of removableEntries) { + await this.remove(entry); + } + } + + /** + * Internal + */ + + private getKey(entry: Pick): string { + const { userOp, chainId } = entry; + return `${chainId}:${userOp.sender.toLowerCase()}:${userOp.nonce}`; + } + + private async fetchAll(): Promise { + const keys = await this.fetchKeys(); + const rawEntries = await this.db + .getMany(keys) + .catch(() => []); + return rawEntries.map(rawEntryToMempoolEntry); + } + + private async fetchKeys(): Promise { + const userOpKeys = await this.db + .get(this.USEROP_COLLECTION_KEY) + .catch(() => []); + return userOpKeys; + } + + private async findByKey(key: string): Promise { + const raw = await this.db.get(key).catch(() => null); + if (raw) { + return rawEntryToMempoolEntry(raw); + } + return null; + } + + private async validateReplaceability( + newEntry: MempoolEntry, + oldEntry?: MempoolEntry | null + ): Promise { + if (!oldEntry) { + oldEntry = await this.find(newEntry); + } + if ( + !oldEntry || + newEntry.canReplaceWithTTL(oldEntry, this.networkConfig.useropsTTL) + ) { + return true; + } + throw new RpcError( + "User op cannot be replaced: fee too low", + RpcErrorCodes.INVALID_USEROP + ); + } + + private async update(entry: MempoolEntry): Promise { + await this.mutex.runExclusive(async () => { + await this.db.put(this.getKey(entry), entry); + }); + } + + private async remove(entry: MempoolEntry | null): Promise { + if (!entry) { + return; + } + await this.mutex.runExclusive(async () => { + const key = this.getKey(entry); + const newKeys = (await this.fetchKeys()).filter((k) => k !== key); + await this.db.del(key); + await this.db.put(this.USEROP_COLLECTION_KEY, newKeys); + this.logger.debug(`${entry.userOpHash} deleted from mempool`); + }); + } + + private async saveUserOpHash( + hash: string, + entry: MempoolEntry + ): Promise { + const key = this.getKey(entry); + await this.db.put(`${this.USEROP_HASHES_COLLECTION_PREFIX}${hash}`, key); + } + + private async removeUserOpHash(hash: string): Promise { + await this.db.del(`${this.USEROP_HASHES_COLLECTION_PREFIX}${hash}`); + } +} diff --git a/packages/executor/src/services/MempoolService/utils.ts b/packages/executor/src/services/MempoolService/utils.ts new file mode 100644 index 00000000..66eed68f --- /dev/null +++ b/packages/executor/src/services/MempoolService/utils.ts @@ -0,0 +1,6 @@ +import { IMempoolEntry } from "../../entities/interfaces"; +import { MempoolEntry } from "../../entities/MempoolEntry"; + +export function rawEntryToMempoolEntry(raw: IMempoolEntry): MempoolEntry { + return new MempoolEntry(raw); +} diff --git a/packages/executor/src/services/P2PService.ts b/packages/executor/src/services/P2PService.ts index a40a8f1b..12b0d513 100644 --- a/packages/executor/src/services/P2PService.ts +++ b/packages/executor/src/services/P2PService.ts @@ -20,22 +20,14 @@ export class P2PService { limit: number, offset: number ): Promise { - let hasMore = false; - let keys = await this.mempoolService.fetchKeys(); - if (keys.length > limit + offset) { - hasMore = true; - } - keys = keys.slice(offset, offset + limit); - - const mempoolEntries = await this.mempoolService.fetchManyByKeys(keys); + const entries = await this.mempoolService.getNewEntriesSorted( + limit, + offset + ); + const hasMore = entries.length == limit; return { - next_cursor: hasMore ? keys.length + offset : 0, - hashes: mempoolEntries - .filter( - (entry) => - this.entryPointService.getEntryPointVersion(entry.entryPoint) === - EntryPointVersion.SIX - ) + next_cursor: hasMore ? entries.length + offset : 0, + hashes: entries .map((entry) => entry.userOpHash) .filter((hash) => hash && hash.length === 66), }; diff --git a/packages/executor/src/services/ReputationService.ts b/packages/executor/src/services/ReputationService.ts index b81ccca0..366aab16 100644 --- a/packages/executor/src/services/ReputationService.ts +++ b/packages/executor/src/services/ReputationService.ts @@ -2,6 +2,7 @@ import { BigNumber, utils } from "ethers"; import { IDbController } from "@skandha/types/lib"; import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; import { ReputationStatus } from "@skandha/types/lib/executor"; +import { Mutex } from "async-mutex"; import { ReputationEntry } from "../entities/ReputationEntry"; import { ReputationEntryDump, @@ -13,6 +14,7 @@ export class ReputationService { private REP_COLL_KEY: string; // prefix in rocksdb private WL_COLL_KEY: string; // whitelist prefix private BL_COLL_KEY: string; // blacklist prefix + private mutex = new Mutex(); constructor( private db: IDbController, @@ -57,15 +59,19 @@ export class ReputationService { } async updateSeenStatus(address: string): Promise { - const entry = await this.fetchOne(address); - entry.addToReputation(1, 0); - await this.save(entry); + await this.mutex.runExclusive(async () => { + const entry = await this.fetchOne(address); + entry.addToReputation(1, 0); + await this.save(entry); + }); } async updateIncludedStatus(address: string): Promise { - const entry = await this.fetchOne(address); - entry.addToReputation(0, 1); - await this.save(entry); + await this.mutex.runExclusive(async () => { + const entry = await this.fetchOne(address); + entry.addToReputation(0, 1); + await this.save(entry); + }); } async getStatus(address: string): Promise { @@ -82,9 +88,11 @@ export class ReputationService { opsSeen: number, opsIncluded: number ): Promise { - const entry = await this.fetchOne(address); - entry.setReputation(opsSeen, opsIncluded); - await this.save(entry); + await this.mutex.runExclusive(async () => { + const entry = await this.fetchOne(address); + entry.setReputation(opsSeen, opsIncluded); + await this.save(entry); + }); } async dump(): Promise { @@ -126,13 +134,15 @@ export class ReputationService { } async clearState(): Promise { - const addresses: string[] = await this.db - .get(this.REP_COLL_KEY) - .catch(() => []); - for (const addr of addresses) { - await this.db.del(this.getKey(addr)); - } - await this.db.del(this.REP_COLL_KEY); + await this.mutex.runExclusive(async () => { + const addresses: string[] = await this.db + .get(this.REP_COLL_KEY) + .catch(() => []); + for (const addr of addresses) { + await this.db.del(this.getKey(addr)); + } + await this.db.del(this.REP_COLL_KEY); + }); } /** diff --git a/packages/executor/src/services/SubscriptionService.ts b/packages/executor/src/services/SubscriptionService.ts new file mode 100644 index 00000000..2a843881 --- /dev/null +++ b/packages/executor/src/services/SubscriptionService.ts @@ -0,0 +1,175 @@ +import EventEmitter from "node:events"; +import { WebSocket } from "ws"; +import { ethers } from "ethers"; +import StrictEventEmitter from "strict-event-emitter-types"; +import { Logger } from "@skandha/types/lib"; +import { deepHexlify } from "@skandha/utils/lib/hexlify"; +import { MempoolEntryStatus } from "@skandha/types/lib/executor"; +import { MempoolEntry } from "../entities/MempoolEntry"; + +export enum ExecutorEvent { + pendingUserOps = "pendingUserOps", // user ops that are in the mempool + submittedUserOps = "submittedUserOps", // user ops submitted onchain, but not yet settled + onChainUserOps = "onChainUserOps", // user ops found onchain + ping = "ping", +} + +export type ExecutorEvents = { + [ExecutorEvent.pendingUserOps]: (entry: MempoolEntry) => void; + [ExecutorEvent.submittedUserOps]: (entry: MempoolEntry) => void; + [ExecutorEvent.onChainUserOps]: (entry: MempoolEntry) => void; + [ExecutorEvent.ping]: () => void; +}; + +export type IExecutorEventBus = StrictEventEmitter< + EventEmitter, + ExecutorEvents +>; + +export class ExecutorEventBus extends (EventEmitter as { + new (): IExecutorEventBus; +}) {} + +export class SubscriptionService { + constructor(private eventBus: ExecutorEventBus, private logger: Logger) { + this.eventBus.on( + ExecutorEvent.pendingUserOps, + this.onPendingUserOps.bind(this) + ); + this.eventBus.on( + ExecutorEvent.submittedUserOps, + this.onSubmittedUserOps.bind(this) + ); + this.eventBus.on( + ExecutorEvent.onChainUserOps, + this.onOnChainUserOps.bind(this) + ); + } + + private events: { + [event in ExecutorEvent]: Set; + } = { + [ExecutorEvent.pendingUserOps]: new Set(), + [ExecutorEvent.submittedUserOps]: new Set(), + [ExecutorEvent.onChainUserOps]: new Set(), + [ExecutorEvent.ping]: new Set(), + }; + private listeners: { [id: string]: WebSocket } = {}; + + listenPendingUserOps(socket: WebSocket): string { + return this.listen(socket, ExecutorEvent.pendingUserOps); + } + + listenSubmittedUserOps(socket: WebSocket): string { + return this.listen(socket, ExecutorEvent.submittedUserOps); + } + + listenOnChainUserOps(socket: WebSocket): string { + return this.listen(socket, ExecutorEvent.onChainUserOps); + } + + listenPing(socket: WebSocket): string { + return this.listen(socket, ExecutorEvent.ping); + } + + unsubscribe(socket: WebSocket, id: string): void { + delete this.listeners[id]; + for (const event in ExecutorEvent) { + this.events[event as ExecutorEvent].delete(id); + } + this.logger.debug(`${id} unsubscribed`); + } + + onPendingUserOps(entry: MempoolEntry): void { + const { userOp, userOpHash, entryPoint, prefund, submittedTime } = entry; + this.propagate(ExecutorEvent.pendingUserOps, { + userOp, + userOpHash, + entryPoint, + prefund, + submittedTime, + status: "Pending", + }); + } + + onSubmittedUserOps(entry: MempoolEntry): void { + const { userOp, userOpHash, entryPoint, transaction, revertReason } = entry; + const status = + Object.keys(MempoolEntryStatus).find( + (status) => + entry.status === + MempoolEntryStatus[status as keyof typeof MempoolEntryStatus] + ) ?? "New"; + this.propagate(ExecutorEvent.submittedUserOps, { + userOp, + userOpHash, + entryPoint, + transaction, + status, + revertReason: revertReason, + }); + } + + onOnChainUserOps(entry: MempoolEntry): void { + const { userOp, userOpHash, entryPoint, actualTransaction } = entry; + this.propagate(ExecutorEvent.onChainUserOps, { + userOp, + userOpHash, + entryPoint, + transaction: actualTransaction, + status: "onChain", + }); + } + + onPing(): void { + this.propagate(ExecutorEvent.ping); + } + + private listen(socket: WebSocket, event: ExecutorEvent): string { + const id = this.generateEventId(); + this.listeners[id] = socket; + this.events[event].add(id); + this.logger.debug(`${id} subscribed for ${event}`); + return id; + } + + private propagate(event: ExecutorEvent, data?: object): void { + if (data != undefined) { + data = deepHexlify(data); + } + for (const id of this.events[event]) { + const response: object = { + jsonrpc: "2.0", + method: "skandha_subscription", + params: { + subscription: id, + result: data, + }, + }; + try { + const socket = this.listeners[id]; + if ( + socket.readyState === WebSocket.CLOSED || + socket.readyState === WebSocket.CLOSING + ) { + this.unsubscribe(socket, id); + return; + } + this.listeners[id].send(JSON.stringify(response)); + } catch (err) { + this.logger.error(err, `Could not send event. Id: ${id}`); + } + } + } + + private generateEventId(): string { + const id = ethers.utils.hexlify(ethers.utils.randomBytes(16)); + for (const event in ExecutorEvent) { + if (this.events[event as ExecutorEvent].has(id)) { + // retry if id already exists + return this.generateEventId(); + } + } + return id; + } +} diff --git a/packages/executor/src/services/UserOpValidation/GethTracer.ts b/packages/executor/src/services/UserOpValidation/GethTracer.ts index 67bbc08a..40eb8e72 100644 --- a/packages/executor/src/services/UserOpValidation/GethTracer.ts +++ b/packages/executor/src/services/UserOpValidation/GethTracer.ts @@ -12,7 +12,10 @@ if (tracer == null) { throw new Error("Tracer not found"); } const regexp = /function \w+\s*\(\s*\)\s*{\s*return\s*(\{[\s\S]+\});?\s*\}\s*$/; -const stringifiedTracer = tracer.match(regexp)![1]; +const stringifiedTracer = tracer + .match(regexp)![1] + .replace(/\r\n/g, "") + .replace(/( ){2,}/g, " "); export class GethTracer { constructor(private provider: providers.JsonRpcProvider) {} diff --git a/packages/executor/src/services/UserOpValidation/service.ts b/packages/executor/src/services/UserOpValidation/service.ts index e4379de4..9a4ea9c9 100644 --- a/packages/executor/src/services/UserOpValidation/service.ts +++ b/packages/executor/src/services/UserOpValidation/service.ts @@ -11,6 +11,7 @@ import { } from "../../interfaces"; import { ReputationService } from "../ReputationService"; import { EntryPointService } from "../EntryPointService"; +import { Skandha } from "../../modules"; import { EstimationService, SafeValidationService, @@ -25,6 +26,7 @@ export class UserOpValidationService { private unsafeValidationService: UnsafeValidationService; constructor( + private skandhaUtils: Skandha, private provider: providers.Provider, private entryPointService: EntryPointService, private reputationService: ReputationService, @@ -41,6 +43,7 @@ export class UserOpValidationService { this.logger ); this.safeValidationService = new SafeValidationService( + this.skandhaUtils, this.provider, this.entryPointService, this.reputationService, diff --git a/packages/executor/src/services/UserOpValidation/validators/estimation.ts b/packages/executor/src/services/UserOpValidation/validators/estimation.ts index bc1576b7..2d712278 100644 --- a/packages/executor/src/services/UserOpValidation/validators/estimation.ts +++ b/packages/executor/src/services/UserOpValidation/validators/estimation.ts @@ -1,5 +1,3 @@ -import RpcError from "@skandha/types/lib/api/errors/rpc-error"; -import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes"; import { providers } from "ethers"; import { Logger } from "@skandha/types/lib"; import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; @@ -25,7 +23,7 @@ export class EstimationService { const { validAfter, validUntil } = mergeValidationDataValues( returnInfo.accountValidationData, returnInfo.paymasterValidationData - ) + ); return { preOpGas: returnInfo.preOpGas, paid: returnInfo.paid, @@ -33,6 +31,6 @@ export class EstimationService { validUntil: validUntil, targetSuccess: returnInfo.targetSuccess, targetResult: returnInfo.targetResult, - } + }; } } diff --git a/packages/executor/src/services/UserOpValidation/validators/safe.ts b/packages/executor/src/services/UserOpValidation/validators/safe.ts index 13915774..b658d992 100644 --- a/packages/executor/src/services/UserOpValidation/validators/safe.ts +++ b/packages/executor/src/services/UserOpValidation/validators/safe.ts @@ -6,6 +6,7 @@ import { Logger } from "@skandha/types/lib"; import { IWhitelistedEntities } from "@skandha/types/lib/executor"; import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; import { AddressZero } from "@skandha/params/lib"; +import { GetGasPriceResponse } from "@skandha/types/lib/api/interfaces"; import { NetworkConfig, StorageMap, @@ -21,6 +22,7 @@ import { import { ReputationService } from "../../ReputationService"; import { EntryPointService } from "../../EntryPointService"; import { decodeRevertReason } from "../../EntryPointService/utils/decodeRevertReason"; +import { Skandha } from "../../../modules"; /** * Some opcodes like: @@ -53,6 +55,7 @@ export class SafeValidationService { private gethTracer: GethTracer; constructor( + private skandhaUtils: Skandha, private provider: providers.Provider, private entryPointService: EntryPointService, private reputationService: ReputationService, @@ -75,6 +78,17 @@ export class SafeValidationService { .add(userOp.verificationGasLimit) .add(userOp.callGasLimit); + let gasPrice: GetGasPriceResponse | null = null; + if (this.networkConfig.gasFeeInSimulation) { + gasPrice = await this.skandhaUtils.getGasPrice(); + gasPrice.maxFeePerGas = ethers.utils.hexValue( + BigNumber.from(gasPrice.maxFeePerGas) + ); + gasPrice.maxPriorityFeePerGas = ethers.utils.hexValue( + BigNumber.from(gasPrice.maxPriorityFeePerGas) + ); + } + const [data, stateOverrides] = this.entryPointService.encodeSimulateValidation(entryPoint, userOp); const tx: providers.TransactionRequest = { @@ -82,6 +96,7 @@ export class SafeValidationService { data, gasLimit: simulationGas, from: AddressZero, + ...gasPrice, }; const traceCall: BundlerCollectorReturn = diff --git a/packages/executor/src/services/index.ts b/packages/executor/src/services/index.ts index 505926fc..f909be03 100644 --- a/packages/executor/src/services/index.ts +++ b/packages/executor/src/services/index.ts @@ -4,3 +4,5 @@ export * from "./BundlingService"; export * from "./ReputationService"; export * from "./P2PService"; export * from "./EntryPointService"; +export * from "./EventsService"; +export * from "./SubscriptionService"; diff --git a/packages/executor/src/utils/index.ts b/packages/executor/src/utils/index.ts index a722f413..654183d7 100644 --- a/packages/executor/src/utils/index.ts +++ b/packages/executor/src/utils/index.ts @@ -1,29 +1,4 @@ import { BytesLike, hexlify } from "ethers/lib/utils"; -/** - * hexlify all members of object, recursively - * @param obj - */ -export function deepHexlify(obj: any): any { - if (typeof obj === "function") { - return undefined; - } - if (obj == null || typeof obj === "string" || typeof obj === "boolean") { - return obj; - // eslint-disable-next-line no-underscore-dangle - } else if (obj._isBigNumber != null || typeof obj !== "object") { - return hexlify(obj).replace(/^0x0/, "0x"); - } - if (Array.isArray(obj)) { - return obj.map((member) => deepHexlify(member)); - } - return Object.keys(obj).reduce( - (set, key) => ({ - ...set, - [key]: deepHexlify(obj[key]), - }), - {} - ); -} export function extractAddrFromInitCode(data?: BytesLike): string | undefined { if (data == null) { diff --git a/packages/executor/test/constants.ts b/packages/executor/test/constants.ts index 9249fad9..d5232e46 100644 --- a/packages/executor/test/constants.ts +++ b/packages/executor/test/constants.ts @@ -1,14 +1,16 @@ import { constants } from "ethers"; import { StakeInfo } from "../src/interfaces"; -export const TestAccountMnemonic = "test test test test test test test test test test test junk"; +export const TestAccountMnemonic = + "test test test test test test test test test test test junk"; export const EntryPointAddress = "0x9b5d240EF1bc8B4930346599cDDFfBD7d7D56db9"; -export const SimpleFactoryAddress = "0xE759fdEAC26252feFd31a044493154ABDd709344"; +export const SimpleFactoryAddress = + "0xE759fdEAC26252feFd31a044493154ABDd709344"; export const DefaultRpcUrl = "http://127.0.0.1:8545"; export const NetworkName = "anvil"; export const ChainId = 31337; export const ZeroStakeInfo: StakeInfo = { addr: constants.AddressZero, stake: constants.Zero, - unstakeDelaySec: constants.Zero -} + unstakeDelaySec: constants.Zero, +}; diff --git a/packages/executor/test/fixtures/getConfig.ts b/packages/executor/test/fixtures/getConfig.ts index 8d0508b4..3e08c51e 100644 --- a/packages/executor/test/fixtures/getConfig.ts +++ b/packages/executor/test/fixtures/getConfig.ts @@ -1,6 +1,10 @@ import { utils } from "ethers"; import { Config } from "../../src/config"; -import { DefaultRpcUrl, EntryPointAddress, TestAccountMnemonic } from "../constants"; +import { + DefaultRpcUrl, + EntryPointAddress, + TestAccountMnemonic, +} from "../constants"; import { ConfigOptions, NetworkConfig } from "../../src/interfaces"; const BaseConfig: ConfigOptions = { @@ -35,25 +39,29 @@ const BaseConfig: ConfigOptions = { pvgMarkup: 0, canonicalMempoolId: "", canonicalEntryPoint: "", + pvgMarkupPercent: 0, + cglMarkupPercent: 0, + vglMarkupPercent: 3000, + fastlaneValidators: [], }, testingMode: false, unsafeMode: false, redirectRpc: false, -} +}; let config: Config, - networkConfig: NetworkConfig, - configUnsafe: Config, - networkConfigUnsafe: NetworkConfig; + networkConfig: NetworkConfig, + configUnsafe: Config, + networkConfigUnsafe: NetworkConfig; export async function getConfigs() { if (!config) { config = await Config.init(BaseConfig); networkConfig = config.getNetworkConfig(); - + configUnsafe = await Config.init({ ...BaseConfig, - unsafeMode: true + unsafeMode: true, }); networkConfigUnsafe = configUnsafe.getNetworkConfig(); } @@ -62,5 +70,5 @@ export async function getConfigs() { networkConfig, configUnsafe, networkConfigUnsafe, - } + }; } diff --git a/packages/executor/test/fixtures/modules.ts b/packages/executor/test/fixtures/modules.ts index ae8f0676..303f0571 100644 --- a/packages/executor/test/fixtures/modules.ts +++ b/packages/executor/test/fixtures/modules.ts @@ -2,8 +2,8 @@ import { Config } from "../../src/config"; import { NetworkConfig } from "../../src/interfaces"; import { ChainId } from "../constants"; import { logger } from "../mocks/logger"; +import { Web3, Debug, Eth } from "../../src/modules"; import { getServices } from "./services"; -import { Web3, Debug, Skandha, Eth } from "../../src/modules"; export async function getModules(config: Config, networkConfig: NetworkConfig) { const provider = config.getNetworkProvider()!; @@ -13,11 +13,12 @@ export async function getModules(config: Config, networkConfig: NetworkConfig) { mempoolService, bundlingService, entryPointService, + skandha, } = await getServices(config, networkConfig); const web3 = new Web3(config, { version: "test", - commit: "commit" + commit: "commit", }); const debug = new Debug( provider, @@ -27,13 +28,6 @@ export async function getModules(config: Config, networkConfig: NetworkConfig) { reputationService, networkConfig ); - const skandha = new Skandha( - entryPointService, - ChainId, - provider, - config, - logger - ); const eth = new Eth( ChainId, provider, @@ -56,5 +50,5 @@ export async function getModules(config: Config, networkConfig: NetworkConfig) { userOpValidationService, mempoolService, bundlingService, - } + }; } diff --git a/packages/executor/test/fixtures/services.ts b/packages/executor/test/fixtures/services.ts index 7ca3de5d..c18fef59 100644 --- a/packages/executor/test/fixtures/services.ts +++ b/packages/executor/test/fixtures/services.ts @@ -1,12 +1,25 @@ import { BigNumber } from "ethers"; import { Config } from "../../src/config"; import { NetworkConfig } from "../../src/interfaces"; -import { BundlingService, EntryPointService, MempoolService, ReputationService, UserOpValidationService } from "../../src/services"; +import { + BundlingService, + EntryPointService, + EventsService, + ExecutorEventBus, + MempoolService, + ReputationService, + SubscriptionService, + UserOpValidationService, +} from "../../src/services"; import { LocalDbController } from "../mocks/database"; import { ChainId } from "../constants"; import { logger } from "../mocks/logger"; +import { Skandha } from "../../src/modules"; -export async function getServices(config: Config, networkConfig: NetworkConfig) { +export async function getServices( + config: Config, + networkConfig: NetworkConfig +) { const provider = config.getNetworkProvider(); const db = new LocalDbController("test"); const reputationService = new ReputationService( @@ -18,30 +31,45 @@ export async function getServices(config: Config, networkConfig: NetworkConfig) BigNumber.from(networkConfig.minStake), networkConfig.minUnstakeDelay ); + + const eventBus = new ExecutorEventBus(); + const subscriptionService = new SubscriptionService(eventBus, logger); + const entryPointService = new EntryPointService( config.chainId, networkConfig, provider, - reputationService, db, logger - ) + ); - const userOpValidationService = new UserOpValidationService( - provider, + const mempoolService = new MempoolService( + db, + ChainId, entryPointService, reputationService, + eventBus, + networkConfig, + logger + ); + + const skandha = new Skandha( + mempoolService, + entryPointService, ChainId, + provider, config, logger ); - const mempoolService = new MempoolService( - db, - ChainId, + const userOpValidationService = new UserOpValidationService( + skandha, + provider, entryPointService, reputationService, - networkConfig + ChainId, + config, + logger ); const bundlingService = new BundlingService( @@ -51,19 +79,33 @@ export async function getServices(config: Config, networkConfig: NetworkConfig) mempoolService, userOpValidationService, reputationService, + eventBus, config, logger, null, "classic" ); + const eventsService = new EventsService( + ChainId, + networkConfig, + reputationService, + mempoolService, + entryPointService, + eventBus, + db, + logger + ); + bundlingService.setBundlingMode("manual"); return { + skandha, reputationService, userOpValidationService, mempoolService, bundlingService, entryPointService, - } -} \ No newline at end of file + eventsService + }; +} diff --git a/packages/executor/test/unit/modules/skandha.test.ts b/packages/executor/test/unit/modules/skandha.test.ts index f9de0c59..6c872ae6 100644 --- a/packages/executor/test/unit/modules/skandha.test.ts +++ b/packages/executor/test/unit/modules/skandha.test.ts @@ -12,9 +12,9 @@ describe("Skandha module", async () => { it("getGasPrice should return actual onchain gas price", async () => { const gasFee = await client.getFeeData(); - const responseFromSkandha = await skandha.getGasPrice(); - expect(gasFee.maxFeePerGas).toEqual(responseFromSkandha.maxFeePerGas); - expect(gasFee.maxPriorityFeePerGas).toEqual(responseFromSkandha.maxPriorityFeePerGas); + const response = await skandha.getGasPrice(); + expect(gasFee.maxFeePerGas).toEqual(response.maxFeePerGas); + expect(gasFee.maxPriorityFeePerGas).toEqual(response.maxPriorityFeePerGas); }); it("getConfig should return all config values and hide sensitive data", async () => { @@ -27,9 +27,11 @@ describe("Skandha module", async () => { "relayers", "relayer", "rpcEndpoint", - "name" + "name", + "kolibriAuthKey", + "echoAuthKey", ]; - for (const [key, value] of Object.entries(networkConfig)) { + for (const [key] of Object.entries(networkConfig)) { if (sensitiveFields.indexOf(key) > -1) continue; if (!configSkandha.hasOwnProperty(key)) { throw new Error(`${key} is not defined in skandha_config`); diff --git a/packages/executor/test/unit/services/BundlingService.test.ts b/packages/executor/test/unit/services/BundlingService.test.ts index fa5d227b..677c409b 100644 --- a/packages/executor/test/unit/services/BundlingService.test.ts +++ b/packages/executor/test/unit/services/BundlingService.test.ts @@ -1,12 +1,20 @@ import { describe, it, expect } from "vitest"; -import { getConfigs, getModules, getClient, getWallet, createSignedUserOp, getCounterFactualAddress } from "../../fixtures"; +import { + getConfigs, + getModules, + getClient, + getWallet, + createSignedUserOp, + getCounterFactualAddress, +} from "../../fixtures"; import { EntryPointAddress } from "../../constants"; import { setBalance } from "../../utils"; describe("Bundling Service", async () => { await getClient(); // runs anvil - const { service, ethModule, networkConfigUnsafe, debugModule } = await prepareTest(); + const { service, ethModule, networkConfigUnsafe, debugModule } = + await prepareTest(); describe("Unsafe mode", async () => { it("Submitted bundle should contain configured number of userops", async () => { const { bundleSize } = networkConfigUnsafe; @@ -18,18 +26,20 @@ describe("Bundling Service", async () => { const userOp = await createSignedUserOp(ethModule, wallet); const hash = await ethModule.sendUserOperation({ userOp, - entryPoint: EntryPointAddress + entryPoint: EntryPointAddress, }); userOpHashes.push(hash); } expect(userOpHashes).toHaveLength(bundleSize); - expect((await debugModule.dumpMempool())).toHaveLength(bundleSize); + expect(await debugModule.dumpMempool()).toHaveLength(bundleSize); expect(await debugModule.sendBundleNow()).toBe("ok"); // check that all userops are in the same bundle let txHash = null; for (let i = 0; i < bundleSize; ++i) { - const response = await ethModule.getUserOperationByHash(userOpHashes[i]); + const response = await ethModule.getUserOperationByHash( + userOpHashes[i] + ); if (!txHash) { txHash = response?.transactionHash; } else { @@ -45,7 +55,7 @@ async function prepareTest() { const { eth: ethModule, bundlingService: service, - debug: debugModule + debug: debugModule, } = await getModules(configUnsafe, networkConfigUnsafe); return { service, ethModule, networkConfigUnsafe, debugModule }; -}; +} diff --git a/packages/executor/test/unit/services/ReputationService.test.ts b/packages/executor/test/unit/services/ReputationService.test.ts index 228cd1be..bdd89f46 100644 --- a/packages/executor/test/unit/services/ReputationService.test.ts +++ b/packages/executor/test/unit/services/ReputationService.test.ts @@ -1,14 +1,14 @@ -import { describe, it } from 'vitest'; -import { utils } from 'ethers'; -import { ReputationStatus } from 'types/src/executor'; -import { randomAddress } from '../../utils'; -import { getClient, getConfigs, getServices } from '../../fixtures'; -import { assert } from 'chai'; -import * as RpcErrorCodes from 'types/src/api/errors/rpc-error-codes'; - -describe('Reputation Service', async () => { +import { describe, it } from "vitest"; +import { utils } from "ethers"; +import { assert } from "chai"; +import * as RpcErrorCodes from "@skandha/types/src/api/errors/rpc-error-codes"; +import { ReputationStatus } from "@skandha/types/src/executor"; +import { randomAddress } from "../../utils"; +import { getClient, getConfigs, getServices } from "../../fixtures"; + +describe("Reputation Service", async () => { await getClient(); - it('The status of a fresh entry should be OK', async () => { + it("The status of a fresh entry should be OK", async () => { const { service } = await prepareTest(); const wallet = randomAddress(); @@ -16,11 +16,11 @@ describe('Reputation Service', async () => { assert.strictEqual( status, ReputationStatus.OK, - 'The reputation status is not displayed OK.' + "The reputation status is not displayed OK." ); }); - it('The status of an entry should be BANNED', async () => { + it("The status of an entry should be BANNED", async () => { const { service } = await prepareTest(); const wallet = randomAddress(); await service.setReputation(wallet.address, 1000000000000000, 100); @@ -29,13 +29,13 @@ describe('Reputation Service', async () => { assert.strictEqual( status, ReputationStatus.BANNED, - 'The reputation status is not displayed BANNED.' + "The reputation status is not displayed BANNED." ); }); - it('Set the reputation with valid details', async () => { - let random1 = Math.floor(Math.random() * 10 + 1); - let random2 = Math.floor(Math.random() * 10 + 1); + it("Set the reputation with valid details", async () => { + const random1 = Math.floor(Math.random() * 10 + 1); + const random2 = Math.floor(Math.random() * 10 + 1); const { service } = await prepareTest(); const wallet = randomAddress(); await service.setReputation(wallet.address, random1, random2); @@ -45,34 +45,34 @@ describe('Reputation Service', async () => { assert.strictEqual( data.chainId, 31337, - 'The chainId value is not displayed correctly in the response of fetchone function.' + "The chainId value is not displayed correctly in the response of fetchone function." ); assert.isNotEmpty( data.address, - 'The address value is displayed empty in the response of fetchone function.' + "The address value is displayed empty in the response of fetchone function." ); assert.strictEqual( data.opsSeen, random1, - 'The _opsSeen value is not displayed correctly in the response of fetchone function.' + "The _opsSeen value is not displayed correctly in the response of fetchone function." ); assert.strictEqual( data.opsIncluded, random2, - 'The _opsIncluded value is not displayed correctly in the response of fetchone function.' + "The _opsIncluded value is not displayed correctly in the response of fetchone function." ); assert.isNumber( data.lastUpdateTime, - 'The lastUpdateTime value is not number in the response of fetchone function.' + "The lastUpdateTime value is not number in the response of fetchone function." ); }); - it('Dump with single address details', async () => { - let random1 = Math.floor(Math.random() * 10 + 1); - let random2 = Math.floor(Math.random() * 10 + 1); + it("Dump with single address details", async () => { + const random1 = Math.floor(Math.random() * 10 + 1); + const random2 = Math.floor(Math.random() * 10 + 1); const { service } = await prepareTest(); const wallet = randomAddress(); await service.setReputation(wallet.address, random1, random2); @@ -80,30 +80,30 @@ describe('Reputation Service', async () => { assert.isNotEmpty( data[0].address, - 'The address value is emplty in the dump response.' + "The address value is emplty in the dump response." ); assert.strictEqual( data[0].opsSeen, random1, - 'The opsSeen value is not displayed equal in the dump response.' + "The opsSeen value is not displayed equal in the dump response." ); assert.strictEqual( data[0].opsIncluded, random2, - 'The opsIncluded value is not displayed equal in the dump response.' + "The opsIncluded value is not displayed equal in the dump response." ); assert.strictEqual( data[0].status, 0, - 'The status value is not displayed equal in the dump response.' + "The status value is not displayed equal in the dump response." ); }); - it('Check the stake with too low stakes', async () => { + it("Check the stake with too low stakes", async () => { const { service } = await prepareTest(); const wallet = randomAddress(); const data = await service.checkStake({ - stake: utils.parseEther('0.00001'), + stake: utils.parseEther("0.00001"), unstakeDelaySec: 2000, addr: wallet.address, }); @@ -111,18 +111,18 @@ describe('Reputation Service', async () => { assert.strictEqual( data.msg, wallet.address + - ' stake 10000000000000 is too low (min=10000000000000000)', - 'The msg value is not displayed correctly in the response of checkStake function with too low stakes.' + " stake 10000000000000 is too low (min=10000000000000000)", + "The msg value is not displayed correctly in the response of checkStake function with too low stakes." ); assert.strictEqual( data.code, RpcErrorCodes.STAKE_DELAY_TOO_LOW, - 'The code value is not displayed correctly in the response of checkStake function with too low stakes.' + "The code value is not displayed correctly in the response of checkStake function with too low stakes." ); }); - it('Check the stake with zero stakes', async () => { + it("Check the stake with zero stakes", async () => { const { service } = await prepareTest(); const wallet = randomAddress(); const data = await service.checkStake({ @@ -133,59 +133,59 @@ describe('Reputation Service', async () => { assert.strictEqual( data.msg, - wallet.address + ' is unstaked', - 'The msg value is not displayed correctly in the response of checkStake function with zero stake.' + wallet.address + " is unstaked", + "The msg value is not displayed correctly in the response of checkStake function with zero stake." ); assert.strictEqual( data.code, RpcErrorCodes.STAKE_DELAY_TOO_LOW, - 'The code value is not displayed correctly in the response of checkStake function with zero stake.' + "The code value is not displayed correctly in the response of checkStake function with zero stake." ); }); - it('Check the stake with less than minUnstakeDelay value', async () => { + it("Check the stake with less than minUnstakeDelay value", async () => { const { service } = await prepareTest(); const wallet = randomAddress(); const data = await service.checkStake({ - stake: utils.parseEther('0.1'), + stake: utils.parseEther("0.1"), unstakeDelaySec: -1, // minUnstakeDelay is given in the config file in packages\executor\test\fixtures\getConfig.ts addr: wallet.address, }); assert.strictEqual( data.msg, - wallet.address + ' unstake delay -1 is too low (min=0)', - 'The msg value is not displayed correctly in the response of checkStake function with less than minUnstakeDelay value.' + wallet.address + " unstake delay -1 is too low (min=0)", + "The msg value is not displayed correctly in the response of checkStake function with less than minUnstakeDelay value." ); assert.strictEqual( data.code, RpcErrorCodes.STAKE_DELAY_TOO_LOW, - 'The code value is not displayed correctly in the response of checkStake function with less than minUnstakeDelay value.' + "The code value is not displayed correctly in the response of checkStake function with less than minUnstakeDelay value." ); }); - it('Check the stake with banned address', async () => { + it("Check the stake with banned address", async () => { const { service } = await prepareTest(); const wallet = randomAddress(); await service.setReputation(wallet.address, 1000000000000000, 100); const data = await service.checkStake({ - stake: utils.parseEther('0.1'), + stake: utils.parseEther("0.1"), unstakeDelaySec: 2000, addr: wallet.address, }); assert.strictEqual( data.msg, - wallet.address + ' is banned', - 'The msg value is not displayed correctly in the response of checkStake function with banned address.' + wallet.address + " is banned", + "The msg value is not displayed correctly in the response of checkStake function with banned address." ); assert.strictEqual( data.code, RpcErrorCodes.PAYMASTER_OR_AGGREGATOR_BANNED, - 'The code value is not displayed correctly in the response of checkStake function with banned address.' + "The code value is not displayed correctly in the response of checkStake function with banned address." ); }); }); diff --git a/packages/executor/tracer.js b/packages/executor/tracer.js index d63bc204..3ac137c9 100644 --- a/packages/executor/tracer.js +++ b/packages/executor/tracer.js @@ -1,5 +1,5 @@ -function bundlerCollectorTracer() { - return { +function tracer() { + return { callsFromEntryPoint: [], currentLevel: null, keccak: [], @@ -8,207 +8,242 @@ function bundlerCollectorTracer() { debug: [], lastOp: "", lastThreeOpcodes: [], - stopCollectingTopic: "bb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972", + stopCollectingTopic: + "bb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972", stopCollecting: false, topLevelCallCounter: 0, - - fault(log, _db) { - this.debug.push("fault depth=", log.getDepth(), " gas=", log.getGas(), " cost=", log.getCost(), " err=", log.getError()) + + fault(log, db) { + this.debug.push( + "fault depth=" + + log.getDepth() + + " gas=" + + log.getGas() + + " cost=" + + log.getCost() + + " err=" + + log.getError() + ); }, - + result(_ctx, _db) { - return { - callsFromEntryPoint: this.callsFromEntryPoint, - keccak: this.keccak, - logs: this.logs, - calls: this.calls, - debug: this.debug - } + return { + callsFromEntryPoint: this.callsFromEntryPoint, + keccak: this.keccak, + logs: this.logs, + calls: this.calls, + debug: this.debug, + }; }, - + enter(frame) { - if (this.stopCollecting) { - return - } - this.calls.push({ - type: frame.getType(), - from: toHex(frame.getFrom()), - to: toHex(frame.getTo()), - method: toHex(frame.getInput()).slice(0, 10), - gas: frame.getGas(), - value: frame.getValue() - }) + if (this.stopCollecting) { + return; + } + this.calls.push({ + type: frame.getType(), + from: toHex(frame.getFrom()), + to: toHex(frame.getTo()), + method: toHex(frame.getInput()).slice(0, 10), + gas: frame.getGas(), + value: frame.getValue(), + }); }, + exit(frame) { - if (this.stopCollecting) { - return - } - this.calls.push({ - type: frame.getError() != null ? "REVERT" : "RETURN", - gasUsed: frame.getGasUsed(), - data: toHex(frame.getOutput()).slice(0, 4000) - }) + if (this.stopCollecting) { + return; + } + this.calls.push({ + type: frame.getError() != null ? "REVERT" : "RETURN", + gasUsed: frame.getGasUsed(), + data: toHex(frame.getOutput()).slice(0, 4000), + }); }, - + countSlot(list, key) { - list[key] = (list[key] || 0) + 1 + list[key] = (list[key] || 0) + 1; }, + + isAllowedPrecompiled(address) { + const addrHex = toHex(address); + const addressInt = parseInt(addrHex); + return addressInt > 0 && addressInt < 10; + }, + step(log, db) { - if (this.stopCollecting) { - return - } - const opcode = log.op.toString() - - const stackSize = log.stack.length() - const stackTop3 = [] - for (let i = 0; i < 3 && i < stackSize; i++) { - stackTop3.push(log.stack.peek(i)) - } - this.lastThreeOpcodes.push({ - opcode, - stackTop3 - }) - if (this.lastThreeOpcodes.length > 3) { - this.lastThreeOpcodes.shift() - } + if (this.stopCollecting) { + return; + } + const opcode = log.op.toString(); + const stackSize = log.stack.length(); + const stackTop3 = []; + for (var i = 0; i < 3 && i < stackSize; i++) { + stackTop3.push(log.stack.peek(i)); + } + this.lastThreeOpcodes.push({ + opcode, + stackTop3, + }); + if (this.lastThreeOpcodes.length > 3) { + this.lastThreeOpcodes.shift(); + } + if ( + log.getGas() < log.getCost() || + (opcode === "SSTORE" && log.getGas() < 2300) + ) { + this.currentLevel.oog = true; + } + + if (opcode === "REVERT" || opcode === "RETURN") { + if (log.getDepth() === 1) { + const ofs = parseInt(log.stack.peek(0).toString()); + const len = parseInt(log.stack.peek(1).toString()); + const data = toHex(log.memory.slice(ofs, ofs + len)).slice(0, 4000); + this.calls.push({ + type: opcode, + gasUsed: 0, + data, + }); + } + this.lastThreeOpcodes = []; + } + + if (log.getDepth() === 1) { + if (opcode === "CALL" || opcode === "STATICCALL") { + const addr = toAddress(log.stack.peek(1).toString(16)); + const topLevelTargetAddress = toHex(addr); + const ofs = parseInt(log.stack.peek(3).toString()); + const topLevelMethodSig = toHex(log.memory.slice(ofs, ofs + 4)); + + this.currentLevel = this.callsFromEntryPoint[ + this.topLevelCallCounter + ] = { + topLevelMethodSig, + topLevelTargetAddress, + access: {}, + opcodes: {}, + extCodeAccessInfo: {}, + contractSize: {}, + }; + this.topLevelCallCounter++; + } else if (opcode === "LOG1") { + const topic = log.stack.peek(2).toString(16); + if (topic === this.stopCollectingTopic) { + this.stopCollecting = true; + } + } + if (opcode.startsWith("LOG")) { + const count = parseInt(opcode.substring(3)); + const ofs = parseInt(log.stack.peek(0).toString()); + const len = parseInt(log.stack.peek(1).toString()); + const topics = []; + for (var i = 0; i < count; i++) { + topics.push("0x" + log.stack.peek(2 + i).toString(16)); + } + const data = toHex(log.memory.slice(ofs, ofs + len)); + this.logs.push({ + topics, + data, + }); + } + this.lastOp = ""; + return; + } + + const lastOpInfo = + this.lastThreeOpcodes[this.lastThreeOpcodes.length - 2]; + if ( + lastOpInfo && + lastOpInfo.opcode && + lastOpInfo.opcode.match(/^(EXT.*)$/) != null + ) { + const addr = toAddress(lastOpInfo.stackTop3[0].toString(16)); + const addrHex = toHex(addr); + const last3opcodesString = this.lastThreeOpcodes + .map(function (x) { + return x.opcode; + }) + .join(" "); + if (last3opcodesString.match(/^(\w+) EXTCODESIZE ISZERO$/) == null) { + this.currentLevel.extCodeAccessInfo[addrHex] = opcode; + } + } + + if ( + opcode.match(/^(EXT.*|CALL|CALLCODE|DELEGATECALL|STATICCALL)$/) != null + ) { + const idx = opcode.startsWith("EXT") ? 0 : 1; + const addr = toAddress(log.stack.peek(idx).toString(16)); + const addrHex = toHex(addr); if ( - log.getGas() < log.getCost() || - (opcode === "SSTORE" && log.getGas() < 2300) + this.currentLevel.contractSize[addrHex] == null && + !this.isAllowedPrecompiled(addr) ) { - this.currentLevel.oog = true - } - - if (opcode === "REVERT" || opcode === "RETURN") { - if (log.getDepth() === 1) { - const ofs = parseInt(log.stack.peek(0).toString()) - const len = parseInt(log.stack.peek(1).toString()) - const data = toHex(log.memory.slice(ofs, ofs + len)).slice(0, 4000) - this.calls.push({ - type: opcode, - gasUsed: 0, - data - }) - } - this.lastThreeOpcodes = [] - } - - if (log.getDepth() === 1) { - if (opcode === "CALL" || opcode === "STATICCALL") { - const addr = toAddress(log.stack.peek(1).toString(16)) - const topLevelTargetAddress = toHex(addr) - const ofs = parseInt(log.stack.peek(3).toString()) - const topLevelMethodSig = toHex(log.memory.slice(ofs, ofs + 4)) - - this.currentLevel = this.callsFromEntryPoint[ - this.topLevelCallCounter - ] = { - topLevelMethodSig, - topLevelTargetAddress, - access: {}, - opcodes: {}, - extCodeAccessInfo: {}, - contractSize: {} - } - this.topLevelCallCounter++ - } else if (opcode === "LOG1") { - const topic = log.stack.peek(2).toString(16) - if (topic === this.stopCollectingTopic) { - this.stopCollecting = true - } - } - this.lastOp = "" - return - } - - const lastOpInfo = this.lastThreeOpcodes[this.lastThreeOpcodes.length - 2] - if (lastOpInfo && lastOpInfo.opcode && lastOpInfo.opcode.match(/^(EXT.*)$/) != null) { - const addr = toAddress(lastOpInfo.stackTop3[0].toString(16)) - const addrHex = toHex(addr) - const last3opcodesString = this.lastThreeOpcodes - .map(x => x.opcode) - .join(" ") - if (last3opcodesString.match(/^(\w+) EXTCODESIZE ISZERO$/) == null) { - this.currentLevel.extCodeAccessInfo[addrHex] = opcode - } - } - const isAllowedPrecompiled = address => { - const addrHex = toHex(address) - const addressInt = parseInt(addrHex) - return addressInt > 0 && addressInt < 10 - } + this.currentLevel.contractSize[addrHex] = { + contractSize: db.getCode(addr).length, + opcode, + }; + } + } + + if (this.lastOp === "GAS" && !opcode.includes("CALL")) { + this.countSlot(this.currentLevel.opcodes, "GAS"); + } + + if (opcode !== "GAS") { if ( - opcode.match(/^(EXT.*|CALL|CALLCODE|DELEGATECALL|STATICCALL)$/) != null + opcode.match( + /^(DUP\d+|PUSH\d+|SWAP\d+|POP|ADD|SUB|MUL|DIV|EQ|LTE?|S?GTE?|SLT|SH[LR]|AND|OR|NOT|ISZERO)$/ + ) == null ) { - const idx = opcode.startsWith("EXT") ? 0 : 1 - const addr = toAddress(log.stack.peek(idx).toString(16)) - const addrHex = toHex(addr) - if ( - this.currentLevel.contractSize[addrHex] == null && - !isAllowedPrecompiled(addr) - ) { - this.currentLevel.contractSize[addrHex] = { - contractSize: db.getCode(addr).length, - opcode - } - } - } - - if (this.lastOp === "GAS" && !opcode.includes("CALL")) { - this.countSlot(this.currentLevel.opcodes, "GAS") - } - if (opcode !== "GAS") { - if ( - opcode.match( - /^(DUP\d+|PUSH\d+|SWAP\d+|POP|ADD|SUB|MUL|DIV|EQ|LTE?|S?GTE?|SLT|SH[LR]|AND|OR|NOT|ISZERO)$/ - ) == null - ) { - this.countSlot(this.currentLevel.opcodes, opcode) - } - } - this.lastOp = opcode - - if (opcode === "SLOAD" || opcode === "SSTORE") { - const slot = toWord(log.stack.peek(0).toString(16)) - const slotHex = toHex(slot) - const addr = log.contract.getAddress() - const addrHex = toHex(addr) - let access = this.currentLevel.access[addrHex] - if (access == null) { - access = { - reads: {}, - writes: {} - } - this.currentLevel.access[addrHex] = access - } - if (opcode === "SLOAD") { - if (access.reads[slotHex] == null && access.writes[slotHex] == null) { - access.reads[slotHex] = toHex(db.getState(addr, slot)) - } - } else { - this.countSlot(access.writes, slotHex) - } - } - - if (opcode === "KECCAK256") { - const ofs = parseInt(log.stack.peek(0).toString()) - const len = parseInt(log.stack.peek(1).toString()) - if (len > 20 && len < 512) { - this.keccak.push(toHex(log.memory.slice(ofs, ofs + len))) - } - } else if (opcode.startsWith("LOG")) { - const count = parseInt(opcode.substring(3)) - const ofs = parseInt(log.stack.peek(0).toString()) - const len = parseInt(log.stack.peek(1).toString()) - const topics = [] - for (let i = 0; i < count; i++) { - topics.push("0x" + log.stack.peek(2 + i).toString(16)) - } - const data = toHex(log.memory.slice(ofs, ofs + len)) - this.logs.push({ - topics, - data - }) - } - } - } -} \ No newline at end of file + this.countSlot(this.currentLevel.opcodes, opcode); + } + } + this.lastOp = opcode; + + if (opcode === "SLOAD" || opcode === "SSTORE") { + const slot = toWord(log.stack.peek(0).toString(16)); + const slotHex = toHex(slot); + const addr = log.contract.getAddress(); + const addrHex = toHex(addr); + var access = this.currentLevel.access[addrHex]; + if (access == null) { + access = { + reads: {}, + writes: {}, + }; + this.currentLevel.access[addrHex] = access; + } + if (opcode === "SLOAD") { + if (access.reads[slotHex] == null && access.writes[slotHex] == null) { + access.reads[slotHex] = toHex(db.getState(addr, slot)); + } + } else { + this.countSlot(access.writes, slotHex); + } + } + + if (opcode === "KECCAK256") { + const ofs = parseInt(log.stack.peek(0).toString()); + const len = parseInt(log.stack.peek(1).toString()); + if (len > 20 && len < 512) { + this.keccak.push(toHex(log.memory.slice(ofs, ofs + len))); + } + } else if (opcode.startsWith("LOG")) { + const count = parseInt(opcode.substring(3)); + const ofs = parseInt(log.stack.peek(0).toString()); + const len = parseInt(log.stack.peek(1).toString()); + const topics = []; + for (var i = 0; i < count; i++) { + topics.push("0x" + log.stack.peek(2 + i).toString(16)); + } + const data = toHex(log.memory.slice(ofs, ofs + len)); + this.logs.push({ + topics, + data, + }); + } + }, + }; + } \ No newline at end of file diff --git a/packages/monitoring/package.json b/packages/monitoring/package.json index 6c990dfc..c2776725 100644 --- a/packages/monitoring/package.json +++ b/packages/monitoring/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "description": "The Monitoring module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://github.com/etherspot/etherspot-bundler#readme", @@ -32,7 +32,7 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@skandha/types": "^2.0.1", - "prom-client": "^14.2.0" + "@skandha/types": "^2.0.2", + "prom-client": "15.1.0" } } diff --git a/packages/monitoring/src/metrics/chain.ts b/packages/monitoring/src/metrics/chain.ts index 58aa81ef..afec7500 100644 --- a/packages/monitoring/src/metrics/chain.ts +++ b/packages/monitoring/src/metrics/chain.ts @@ -6,6 +6,9 @@ export interface IChainMetrics { useropsSubmitted: Counter.Internal; useropsEstimated: Counter.Internal; useropsTimeToProcess: Histogram.Internal<"chainId">; + bundlesSubmitted: Counter.Internal; + bundlesFailed: Counter.Internal; + useropsInBundle: Histogram.Internal<"chainId">; } const useropsInMempool = new Counter({ @@ -26,6 +29,18 @@ const useropsSubmitted = new Counter({ labelNames: ["chainId"], }); +const bundlesSubmitted = new Counter({ + name: "skandha_bundles_submitted_count", + help: "Number of bundles successfully submitted on-chain", + labelNames: ["chainId"], +}); + +const bundlesFailed = new Counter({ + name: "skandha_bundles_failed_count", + help: "Number of bundles failed to submit", + labelNames: ["chainId"], +}); + const useropsEstimated = new Counter({ name: "skandha_user_ops_estimated_count", help: "Number of user ops estimated", @@ -36,7 +51,16 @@ const useropsTimeToProcess = new Histogram({ name: "skandha_user_op_time_to_process", help: "How long did it take for userop to get submitted", labelNames: ["chainId"], - buckets: [1, 2, 3, 5, 10, 12, 15, 17, 20, 25, 30, 60, 120, 180], + buckets: [ + 1, 2, 3, 5, 10, 12, 15, 17, 20, 25, 30, 60, 120, 180, 210, 240, 270, 300, + ], +}); + +const useropsInBundle = new Histogram({ + name: "skandha_userops_in_bundle", + help: "Number of bundles with x userops", + labelNames: ["chainId"], + buckets: [1, 2, 3, 4], }); /** @@ -51,6 +75,9 @@ export function createChainMetrics( registry.registerMetric(useropsSubmitted); registry.registerMetric(useropsEstimated); registry.registerMetric(useropsTimeToProcess); + registry.registerMetric(bundlesFailed); + registry.registerMetric(bundlesSubmitted); + registry.registerMetric(useropsInBundle); return { useropsInMempool: useropsInMempool.labels({ chainId }), @@ -58,5 +85,8 @@ export function createChainMetrics( useropsSubmitted: useropsSubmitted.labels({ chainId }), useropsEstimated: useropsEstimated.labels({ chainId }), useropsTimeToProcess: useropsTimeToProcess.labels({ chainId }), + bundlesSubmitted: bundlesSubmitted.labels({ chainId }), + bundlesFailed: bundlesFailed.labels({ chainId }), + useropsInBundle: useropsInBundle.labels({ chainId }), }; } diff --git a/packages/node/package.json b/packages/node/package.json index 60f2b03d..b792b303 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "description": "The bundler node module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -56,13 +56,13 @@ "@libp2p/prometheus-metrics": "1.1.3", "@libp2p/tcp": "6.1.0", "@multiformats/multiaddr": "11.4.0", - "@skandha/api": "^2.0.1", - "@skandha/db": "^2.0.1", - "@skandha/executor": "^2.0.1", - "@skandha/monitoring": "^2.0.1", - "@skandha/params": "^2.0.1", - "@skandha/types": "^2.0.1", - "@skandha/utils": "^2.0.1", + "@skandha/api": "^2.0.2", + "@skandha/db": "^2.0.2", + "@skandha/executor": "^2.0.2", + "@skandha/monitoring": "^2.0.2", + "@skandha/params": "^2.0.2", + "@skandha/types": "^2.0.2", + "@skandha/utils": "^2.0.2", "abstract-leveldown": "7.2.0", "datastore-core": "8.0.1", "ethers": "5.7.2", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 6a23b2c5..f14fb04d 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -135,10 +135,8 @@ export class BundlerNode { await relayerDb.start(); const server = await Server.init({ - enableRequestLogging: nodeOptions.api.enableRequestLogging, - port: nodeOptions.api.port, + ...nodeOptions.api, host: nodeOptions.api.address, - cors: nodeOptions.api.cors, }); metricsOptions.enable @@ -151,7 +149,7 @@ export class BundlerNode { : null; const bundler = new ApiApp({ - server: server.application, + server, config: relayersConfig, testingMode, redirectRpc, diff --git a/packages/params/package.json b/packages/params/package.json index 7ff43cc8..822034bf 100644 --- a/packages/params/package.json +++ b/packages/params/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "description": "Various bundler parameters", "author": "Etherspot", "homepage": "https://github.com/etherspot/skandha#readme", @@ -27,10 +27,9 @@ "dependencies": { "@arbitrum/sdk": "3.1.4", "@chainsafe/ssz": "0.10.1", - "@eth-optimism/sdk": "3.0.0", "@mantleio/sdk": "0.2.1", - "@skandha/types": "^2.0.1", - "@skandha/utils": "^2.0.1", + "@skandha/types": "^2.0.2", + "@skandha/utils": "^2.0.2", "ethers": "5.7.2" }, "scripts": { diff --git a/packages/params/src/eip1559.ts b/packages/params/src/eip1559.ts index cd4f09ab..4621a7a3 100644 --- a/packages/params/src/eip1559.ts +++ b/packages/params/src/eip1559.ts @@ -1,6 +1,5 @@ export const chainsWithoutEIP1559: number[] = [ 122, // "fuse" - 123, // "fuseSparknet" 56, // "bsc" 97, // "bscTest" 1442, // "polygonzkevm" @@ -9,4 +8,6 @@ export const chainsWithoutEIP1559: number[] = [ 534352, // "scroll" 534353, // "scrollAlpha" 534351, // "scrollSepolia" + 31, // rootstock testnet + 30, // rootstock mainnet ]; diff --git a/packages/params/src/gas-estimation/ancient8.ts b/packages/params/src/gas-estimation/ancient8.ts deleted file mode 100644 index c56b1352..00000000 --- a/packages/params/src/gas-estimation/ancient8.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { BigNumber, BigNumberish, Contract } from "ethers"; -import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; -import { serializeTransaction } from "ethers/lib/utils"; -import { IPVGEstimatorWrapper, IPVGEstimator } from "../types/IPVGEstimator"; - -export const estimateAncient8PVG: IPVGEstimatorWrapper = ( - provider -): IPVGEstimator => { - return async ( - contractAddr: string, - data: string, - initial: BigNumberish, - options?: { - contractCreation?: boolean; - userOp?: UserOperation; - } - ): Promise => { - const { chainId } = await provider.getNetwork(); - const latestBlock = await provider.getBlock("latest"); - if (latestBlock.baseFeePerGas == null) { - throw new Error("no base fee"); - } - - const serializedTx = serializeTransaction( - { - to: contractAddr, - chainId: chainId, - nonce: 999999, - gasLimit: BigNumber.from(2).pow(64).sub(1), // maxUint64 - gasPrice: BigNumber.from(2).pow(64).sub(1), // maxUint64 - data: data, - }, - { - r: "0x123451234512345123451234512345123451234512345123451234512345", - s: "0x123451234512345123451234512345123451234512345123451234512345", - v: 28, - } - ); - const gasOracle = new Contract(GAS_ORACLE, GasOracleABI, provider); - const l1GasCost = BigNumber.from( - await gasOracle.callStatic.getL1Fee(serializedTx) - ); - - let maxFeePerGas = BigNumber.from(0); - let maxPriorityFeePerGas = BigNumber.from(0); - if (options && options.userOp) { - const { userOp } = options; - maxFeePerGas = BigNumber.from(userOp.maxFeePerGas); - maxPriorityFeePerGas = BigNumber.from(userOp.maxPriorityFeePerGas); - } - const l2MaxFee = BigNumber.from(maxFeePerGas); - const l2PriorityFee = latestBlock.baseFeePerGas.add(maxPriorityFeePerGas); - const l2Price = l2MaxFee.lt(l2PriorityFee) ? l2MaxFee : l2PriorityFee; - return l1GasCost.div(l2Price).add(initial); - }; -}; - -const GAS_ORACLE = "0x420000000000000000000000000000000000000F"; - -const GasOracleABI = [ - { - inputs: [{ internalType: "bytes", name: "_data", type: "bytes" }], - name: "getL1Fee", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, -]; diff --git a/packages/params/src/gas-estimation/index.ts b/packages/params/src/gas-estimation/index.ts index 0bfb5f11..190f259b 100644 --- a/packages/params/src/gas-estimation/index.ts +++ b/packages/params/src/gas-estimation/index.ts @@ -1,4 +1,3 @@ export * from "./arbitrum"; export * from "./optimism"; export * from "./mantle"; -export * from "./ancient8"; diff --git a/packages/params/src/gas-estimation/optimism.ts b/packages/params/src/gas-estimation/optimism.ts index 5e57d012..83aa39c8 100644 --- a/packages/params/src/gas-estimation/optimism.ts +++ b/packages/params/src/gas-estimation/optimism.ts @@ -1,7 +1,7 @@ -import { BigNumber, BigNumberish } from "ethers"; -import { estimateL1GasCost } from "@eth-optimism/sdk"; +import { BigNumber, BigNumberish, Contract } from "ethers"; import { UserOperation } from "@skandha/types/lib/contracts/UserOperation"; -import { IPVGEstimator, IPVGEstimatorWrapper } from "../types/IPVGEstimator"; +import { serializeTransaction } from "ethers/lib/utils"; +import { IPVGEstimatorWrapper, IPVGEstimator } from "../types/IPVGEstimator"; export const estimateOptimismPVG: IPVGEstimatorWrapper = ( provider @@ -15,25 +15,54 @@ export const estimateOptimismPVG: IPVGEstimatorWrapper = ( userOp?: UserOperation; } ): Promise => { - try { - const latestBlock = await provider.getBlock("latest"); - if (latestBlock.baseFeePerGas == null) { - throw new Error("no base fee"); - } - const l1GasCost = await estimateL1GasCost(provider, { + const { chainId } = await provider.getNetwork(); + const latestBlock = await provider.getBlock("latest"); + if (latestBlock.baseFeePerGas == null) { + throw new Error("no base fee"); + } + + const serializedTx = serializeTransaction( + { to: contractAddr, + chainId: chainId, + nonce: 999999, + gasLimit: BigNumber.from(2).pow(64).sub(1), // maxUint64 + gasPrice: BigNumber.from(2).pow(64).sub(1), // maxUint64 data: data, - }); - const l2MaxFee = BigNumber.from(options!.userOp!.maxFeePerGas); - const l2PriorityFee = latestBlock.baseFeePerGas.add( - options!.userOp!.maxPriorityFeePerGas - ); - const l2Price = l2MaxFee.lt(l2PriorityFee) ? l2MaxFee : l2PriorityFee; - return l1GasCost.div(l2Price).add(initial); - } catch (err) { - // eslint-disable-next-line no-console - console.error("Error while estimating optimism PVG", err); - return BigNumber.from(initial); + }, + { + r: "0x123451234512345123451234512345123451234512345123451234512345", + s: "0x123451234512345123451234512345123451234512345123451234512345", + v: 28, + } + ); + const gasOracle = new Contract(GAS_ORACLE, GasOracleABI, provider); + const l1GasCost = BigNumber.from( + await gasOracle.callStatic.getL1Fee(serializedTx) + ); + + let maxFeePerGas = BigNumber.from(0); + let maxPriorityFeePerGas = BigNumber.from(0); + if (options && options.userOp) { + const { userOp } = options; + maxFeePerGas = BigNumber.from(userOp.maxFeePerGas); + maxPriorityFeePerGas = BigNumber.from(userOp.maxPriorityFeePerGas); } + const l2MaxFee = BigNumber.from(maxFeePerGas); + const l2PriorityFee = latestBlock.baseFeePerGas.add(maxPriorityFeePerGas); + const l2Price = l2MaxFee.lt(l2PriorityFee) ? l2MaxFee : l2PriorityFee; + return l1GasCost.div(l2Price).add(initial); }; }; + +const GAS_ORACLE = "0x420000000000000000000000000000000000000F"; + +const GasOracleABI = [ + { + inputs: [{ internalType: "bytes", name: "_data", type: "bytes" }], + name: "getL1Fee", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +]; diff --git a/packages/params/src/gas-price-oracles/getGasFee.ts b/packages/params/src/gas-price-oracles/getGasFee.ts index 0b118e2d..51210b38 100644 --- a/packages/params/src/gas-price-oracles/getGasFee.ts +++ b/packages/params/src/gas-price-oracles/getGasFee.ts @@ -19,7 +19,8 @@ export const getGasFee = async ( try { const feeData = await provider.getFeeData(); return { - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? feeData.gasPrice ?? 0, + maxPriorityFeePerGas: + feeData.maxPriorityFeePerGas ?? feeData.gasPrice ?? 0, maxFeePerGas: feeData.maxFeePerGas ?? feeData.gasPrice ?? 0, gasPrice: feeData.gasPrice ?? 0, }; diff --git a/packages/params/src/gas-price-oracles/oracles/base.ts b/packages/params/src/gas-price-oracles/oracles/base.ts new file mode 100644 index 00000000..f51cac8f --- /dev/null +++ b/packages/params/src/gas-price-oracles/oracles/base.ts @@ -0,0 +1,7 @@ +import { IGetGasFeeResult, IOracle } from "./interfaces"; +import { getEtherscanGasFee } from "./utils"; + +export const getBaseGasFee: IOracle = ( + apiKey: string | undefined +): Promise => + getEtherscanGasFee("https://api.basescan.org/api", apiKey); diff --git a/packages/params/src/gas-price-oracles/oracles/index.ts b/packages/params/src/gas-price-oracles/oracles/index.ts index d834239e..2385d3d0 100644 --- a/packages/params/src/gas-price-oracles/oracles/index.ts +++ b/packages/params/src/gas-price-oracles/oracles/index.ts @@ -8,6 +8,7 @@ import { getOptimismGasFee } from "./optimism"; import { IOracle } from "./interfaces"; import { getMantleGasFee } from "./mantle"; import { getAncient8GasFee } from "./ancient8"; +import { getBaseGasFee } from "./base"; export const oracles: { [chainId: number]: IOracle | undefined; @@ -19,4 +20,6 @@ export const oracles: { 5000: getMantleGasFee, 5001: getMantleGasFee, 888888888: getAncient8GasFee, + 8453: getBaseGasFee, + 5003: getMantleGasFee, }; diff --git a/packages/types/package.json b/packages/types/package.json index 05bd250c..08b9047d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "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 604867fe..ac68e03a 100644 --- a/packages/types/src/api/interfaces.ts +++ b/packages/types/src/api/interfaces.ts @@ -2,23 +2,21 @@ import { BigNumberish, providers } from "ethers"; import { IWhitelistedEntities } from "../executor"; import { UserOperation } from "../contracts/UserOperation"; -export type EstimatedUserOperationGas = - & { - preVerificationGas: BigNumberish; - verificationGas: BigNumberish; - verificationGasLimit: BigNumberish; - callGasLimit: BigNumberish; - validAfter?: BigNumberish; - validUntil?: BigNumberish; - } - & GetGasPriceResponse; +export type EstimatedUserOperationGas = { + preVerificationGas: BigNumberish; + verificationGas: BigNumberish; + verificationGasLimit: BigNumberish; + callGasLimit: BigNumberish; + validAfter?: BigNumberish; + validUntil?: BigNumberish; +} & GetGasPriceResponse; export type UserOperationByHashResponse = { userOperation: UserOperation; entryPoint: string; - blockNumber: number; - blockHash: string; - transactionHash: string; + blockNumber?: number; + blockHash?: string; + transactionHash?: string; }; export type GetGasPriceResponse = { @@ -58,7 +56,8 @@ export type GetConfigResponse = { throttlingSlack: number; banSlack: number; minSignerBalance: string; - minStake: string; + minStake: BigNumberish | undefined; + minUnstakeDelay: number; multicall: string; estimationStaticBuffer: number; validationGasLimit: number; @@ -76,10 +75,22 @@ export type GetConfigResponse = { relayingMode: string; bundleInterval: number; bundleSize: number; - minUnstakeDelay: number; pvgMarkup: number; canonicalMempoolId: string; canonicalEntryPoint: string; + gasFeeInSimulation: boolean; + skipBundleValidation: boolean; + cglMarkup: number; + vglMarkup: number; + fastlaneValidators: string[]; + archiveDuration: number; + estimationGasLimit: number; + pvgMarkupPercent: number; + cglMarkupPercent: number; + vglMarkupPercent: number; + userOpGasLimit: number; + bundleGasLimit: number; + merkleApiURL: string; }; export type SupportedEntryPoints = string[]; @@ -93,4 +104,14 @@ export interface ServerConfig { port: number; host: string; cors: string; + ws: boolean; + wsPort: number; } + +export type UserOperationStatus = { + userOp: UserOperation; + entryPoint: string; + status: string; + transaction?: string; + reason?: string; +}; diff --git a/packages/types/src/executor/entities/MempoolEntry.ts b/packages/types/src/executor/entities/MempoolEntry.ts index 258b236b..ad2f423f 100644 --- a/packages/types/src/executor/entities/MempoolEntry.ts +++ b/packages/types/src/executor/entities/MempoolEntry.ts @@ -2,7 +2,8 @@ export enum MempoolEntryStatus { New = 0, Pending = 1, Submitted = 2, - IncludedToChain = 3, + OnChain = 3, Finalized = 4, Cancelled = 5, + Reverted = 6, } diff --git a/packages/types/src/executor/index.ts b/packages/types/src/executor/index.ts index dba509d8..6812901e 100644 --- a/packages/types/src/executor/index.ts +++ b/packages/types/src/executor/index.ts @@ -4,7 +4,13 @@ export type SkandhaVersion = { commit: string; }; -export type RelayingMode = "flashbots" | "classic"; +export type RelayingMode = + | "merkle" + | "flashbots" + | "classic" + | "kolibri" + | "echo" + | "fastlane"; export interface SendBundleReturn { transactionHash: string; userOpHashes: string[]; diff --git a/packages/types/src/options/api.ts b/packages/types/src/options/api.ts index 47338b18..e6b34a4e 100644 --- a/packages/types/src/options/api.ts +++ b/packages/types/src/options/api.ts @@ -3,6 +3,8 @@ export type ApiOptions = { address: string; port: number; enableRequestLogging: boolean; + ws: boolean; + wsPort: number; }; export const defaultApiOptions: ApiOptions = { @@ -10,4 +12,6 @@ export const defaultApiOptions: ApiOptions = { address: "0.0.0.0", port: 14337, enableRequestLogging: false, + ws: true, + wsPort: 14337, }; diff --git a/packages/utils/package.json b/packages/utils/package.json index babdaa75..82d17783 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -4,7 +4,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.0.2", "description": "utils of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -33,10 +33,11 @@ }, "dependencies": { "@chainsafe/as-sha256": "0.3.1", - "@skandha/types": "^2.0.1", + "@skandha/types": "^2.0.2", "any-signal": "3.0.1", "bigint-buffer": "1.1.5", "case": "^1.6.3", + "ethers": "5.7.2", "pino": "8.11.0", "pino-pretty": "10.0.0" } diff --git a/packages/utils/src/hexlify.ts b/packages/utils/src/hexlify.ts new file mode 100644 index 00000000..91c03e76 --- /dev/null +++ b/packages/utils/src/hexlify.ts @@ -0,0 +1,28 @@ +import { hexlify } from "ethers/lib/utils"; + +/** + * hexlify all members of object, recursively + * @param obj + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function deepHexlify(obj: any): any { + if (typeof obj === "function") { + return undefined; + } + if (obj == null || typeof obj === "string" || typeof obj === "boolean") { + return obj; + // eslint-disable-next-line no-underscore-dangle + } else if (obj._isBigNumber != null || typeof obj !== "object") { + return hexlify(obj).replace(/^0x0/, "0x"); + } + if (Array.isArray(obj)) { + return obj.map((member) => deepHexlify(member)); + } + return Object.keys(obj).reduce( + (set, key) => ({ + ...set, + [key]: deepHexlify(obj[key]), + }), + {} + ); +} diff --git a/yarn.lock b/yarn.lock index 4dd21df6..94c557fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -367,6 +367,18 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz#c8285183dbdb17008578dbacb6e22748709b4822" integrity sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA== +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1": + version "4.10.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.1.tgz#361461e5cb3845d874e61731c11cfedd664d83a0" + integrity sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA== + "@eslint/eslintrc@^1.3.3": version "1.4.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" @@ -382,97 +394,6 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eth-optimism/contracts-bedrock@0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@eth-optimism/contracts-bedrock/-/contracts-bedrock-0.15.0.tgz#e4cf596c416de5efb4ca7339be889708868c9d17" - integrity sha512-p6whbEsxENrsJ7OkxtPa39BdgmQVeRRquEjt7Qj4oNCooBSeXcWPbx+AtKO/UkODXekuT3inpeWk7QZqmaH0IQ== - dependencies: - "@eth-optimism/core-utils" "^0.12.1" - "@openzeppelin/contracts" "4.7.3" - "@openzeppelin/contracts-upgradeable" "4.7.3" - ethers "^5.7.0" - -"@eth-optimism/contracts@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@eth-optimism/contracts/-/contracts-0.6.0.tgz#15ae76222a9b4d958a550cafb1960923af613a31" - integrity sha512-vQ04wfG9kMf1Fwy3FEMqH2QZbgS0gldKhcBeBUPfO8zu68L61VI97UDXmsMQXzTsEAxK8HnokW3/gosl4/NW3w== - dependencies: - "@eth-optimism/core-utils" "0.12.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - -"@eth-optimism/core-utils@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.12.0.tgz#6337e4599a34de23f8eceb20378de2a2de82b0ea" - integrity sha512-qW+7LZYCz7i8dRa7SRlUKIo1VBU8lvN0HeXCxJR+z+xtMzMQpPds20XJNCMclszxYQHkXY00fOT6GvFw9ZL6nw== - dependencies: - "@ethersproject/abi" "^5.7.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/contracts" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/providers" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/web" "^5.7.0" - bufio "^1.0.7" - chai "^4.3.4" - -"@eth-optimism/core-utils@0.12.1": - version "0.12.1" - resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.12.1.tgz#2d65601e220d1b697eb9b9fe9c1d2df06c1f559a" - integrity sha512-H2NnH9HTVDJmr9Yzb5R7GrAaYimcyIY4bF5Oud0xM1/DP4pSoNMtWm1kG3ZBpdqHFFYWH9GiSZZC5/cjFdKBEA== - dependencies: - "@ethersproject/abi" "^5.7.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/contracts" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/web" "^5.7.0" - chai "^4.3.4" - -"@eth-optimism/core-utils@^0.12.1": - version "0.12.3" - resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.12.3.tgz#7799292ee2155d6f5dd67e2ee58e4a1fbcfd87d4" - integrity sha512-NzHai4HCWbHbnuu4HJaDhT8aGGZXyrKqgteHGhOPKGHRl3DTuuisD/baFq1XVck6X0iHjDihQOLuygtxkdL19A== - dependencies: - "@ethersproject/abi" "^5.7.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/contracts" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/web" "^5.7.1" - chai "^4.3.7" - ethers "^5.7.2" - node-fetch "^2.6.7" - -"@eth-optimism/sdk@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@eth-optimism/sdk/-/sdk-3.0.0.tgz#cba13b85d7b634a1f1f66e1daca3a8603d8aad64" - integrity sha512-tl5Rh1rRBuohSc0/EZgQFVoICFF9Hv+gHWuf5EcI9+xZXEC0F3gFUbTk7VtkyKW0gHNIRL/H+I7Cu0us9eEu+g== - dependencies: - "@eth-optimism/contracts" "0.6.0" - "@eth-optimism/contracts-bedrock" "0.15.0" - "@eth-optimism/core-utils" "0.12.1" - lodash "^4.17.21" - merkletreejs "^0.2.27" - rlp "^2.2.7" - "@ethereumjs/rlp@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" @@ -575,7 +496,7 @@ dependencies: "@ethersproject/bignumber" "^5.7.0" -"@ethersproject/contracts@5.7.0", "@ethersproject/contracts@^5.7.0": +"@ethersproject/contracts@5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== @@ -678,7 +599,7 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.6.8", "@ethersproject/providers@^5.7.0": +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.6.8": version "5.7.2" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== @@ -807,7 +728,7 @@ "@ethersproject/transactions" "^5.7.0" "@ethersproject/wordlists" "^5.7.0" -"@ethersproject/web@5.7.1", "@ethersproject/web@^5.6.1", "@ethersproject/web@^5.7.0", "@ethersproject/web@^5.7.1": +"@ethersproject/web@5.7.1", "@ethersproject/web@^5.6.1", "@ethersproject/web@^5.7.0": version "5.7.1" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== @@ -847,31 +768,47 @@ ajv-formats "^2.1.1" fast-uri "^2.0.0" -"@fastify/cors@8.2.1": - version "8.2.1" - resolved "https://registry.yarnpkg.com/@fastify/cors/-/cors-8.2.1.tgz#dd348162bcbfb87dff4b492e2bef32d41244006a" - integrity sha512-2H2MrDD3ea7g707g1CNNLWb9/tYbmw7HS+MK2SDcgjxwzbOFR93JortelTIO8DBFsZqFtEpKNxiZfSyrGgYcbw== +"@fastify/cors@9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@fastify/cors/-/cors-9.0.1.tgz#9ddb61b4a61e02749c5c54ca29f1c646794145be" + integrity sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q== dependencies: fastify-plugin "^4.0.0" - mnemonist "0.39.5" + mnemonist "0.39.6" "@fastify/deepmerge@^1.0.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a" integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A== -"@fastify/error@^3.0.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@fastify/error/-/error-3.3.0.tgz#eba790082e1144bfc8def0c2c8ef350064bc537b" - integrity sha512-dj7vjIn1Ar8sVXj2yAXiMNCJDmS9MQ9XMlIecX2dIzzhjSHCyKo4DdXjXMs7wKW2kj6yvVRSpuQjOZ3YLrh56w== +"@fastify/error@^3.3.0", "@fastify/error@^3.4.0": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@fastify/error/-/error-3.4.1.tgz#b14bb4cac3dd4ec614becbc643d1511331a6425c" + integrity sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ== -"@fastify/fast-json-stringify-compiler@^4.1.0": +"@fastify/fast-json-stringify-compiler@^4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz#5df89fa4d1592cbb8780f78998355feb471646d5" integrity sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA== dependencies: fast-json-stringify "^5.7.0" +"@fastify/merge-json-schemas@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz#3551857b8a17a24e8c799e9f51795edb07baa0bc" + integrity sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA== + dependencies: + fast-deep-equal "^3.1.3" + +"@fastify/websocket@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@fastify/websocket/-/websocket-10.0.1.tgz#ece72340870dfccc0d5abdbe7242c632a5f3340a" + integrity sha512-8/pQIxTPRD8U94aILTeJ+2O3el/r19+Ej5z1O1mXlqplsUH7KzCjAI0sgd5DM/NoPjAi5qLFNIjgM5+9/rGSNw== + dependencies: + duplexify "^4.1.2" + fastify-plugin "^4.0.0" + ws "^8.0.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" @@ -2165,15 +2102,10 @@ dependencies: "@octokit/openapi-types" "^18.0.0" -"@openzeppelin/contracts-upgradeable@4.7.3": - version "4.7.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.7.3.tgz#f1d606e2827d409053f3e908ba4eb8adb1dd6995" - integrity sha512-+wuegAMaLcZnLCJIvrVUDzA9z/Wp93f0Dla/4jJvIhijRrPabjQbZe6fWiECLaJyfn5ci9fqf9vTw3xpQOad2A== - -"@openzeppelin/contracts@4.7.3": - version "4.7.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.7.3.tgz#939534757a81f8d69cc854c7692805684ff3111e" - integrity sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw== +"@opentelemetry/api@^1.4.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.8.0.tgz#5aa7abb48f23f693068ed2999ae627d2f7d902ec" + integrity sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w== "@parcel/watcher@2.0.4": version "2.0.4" @@ -2614,13 +2546,6 @@ dependencies: "@types/node" "*" -"@types/connect@3.4.35": - version "3.4.35" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== - dependencies: - "@types/node" "*" - "@types/dns-packet@*": version "5.6.1" resolved "https://registry.yarnpkg.com/@types/dns-packet/-/dns-packet-5.6.1.tgz#87df594b65b076d5ce61becbeb3f9afa0962a09a" @@ -2668,10 +2593,10 @@ resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== -"@types/json-schema@^7.0.9": - version "7.0.13" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" - integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/json5@^0.0.29": version "0.0.29" @@ -2754,10 +2679,10 @@ "@types/abstract-leveldown" "*" "@types/node" "*" -"@types/semver@^7.3.12": - version "7.5.3" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" - integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== "@types/send@*": version "0.17.2" @@ -2815,6 +2740,13 @@ dependencies: "@types/node" "*" +"@types/ws@8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21" + integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.1.tgz#07773d7160494d56aa882d7531aac7319ea67c3b" @@ -2827,88 +2759,91 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.43.0.tgz#4a5248eb31b454715ddfbf8cfbf497529a0a78bc" - integrity sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA== +"@typescript-eslint/eslint-plugin@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== dependencies: - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/type-utils" "5.43.0" - "@typescript-eslint/utils" "5.43.0" + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/parser@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.43.0.tgz#9c86581234b88f2ba406f0b99a274a91c11630fd" - integrity sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug== - dependencies: - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/typescript-estree" "5.43.0" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== + dependencies: + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz#566e46303392014d5d163704724872e1f2dd3c15" - integrity sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/visitor-keys" "5.43.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/type-utils@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.43.0.tgz#91110fb827df5161209ecca06f70d19a96030be6" - integrity sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg== +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== dependencies: - "@typescript-eslint/typescript-estree" "5.43.0" - "@typescript-eslint/utils" "5.43.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" debug "^4.3.4" - tsutils "^3.21.0" + ts-api-utils "^1.0.1" -"@typescript-eslint/types@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.43.0.tgz#e4ddd7846fcbc074325293515fa98e844d8d2578" - integrity sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/typescript-estree@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz#b6883e58ba236a602c334be116bfc00b58b3b9f2" - integrity sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/visitor-keys" "5.43.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.43.0.tgz#00fdeea07811dbdf68774a6f6eacfee17fcc669f" - integrity sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A== - dependencies: - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/typescript-estree" "5.43.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - semver "^7.3.7" - -"@typescript-eslint/visitor-keys@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz#cbbdadfdfea385310a20a962afda728ea106befa" - integrity sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg== - dependencies: - "@typescript-eslint/types" "5.43.0" - eslint-visitor-keys "^3.3.0" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + dependencies: + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" "@vitest/coverage-v8@0.34.6": version "0.34.6" @@ -3107,6 +3042,13 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -3196,11 +3138,6 @@ anymatch@~3.1.2: resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== -archy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" - integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== - are-we-there-yet@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" @@ -3325,14 +3262,13 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -avvio@^8.2.0: - version "8.2.1" - resolved "https://registry.yarnpkg.com/avvio/-/avvio-8.2.1.tgz#b5a482729847abb84d5aadce06511c04a0a62f82" - integrity sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw== +avvio@^8.2.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/avvio/-/avvio-8.3.2.tgz#cb5844a612e8421d1f3aef8895ef7fa12f73563f" + integrity sha512-st8e519GWHa/azv8S87mcJvZs4WsgTBjOw/Ih1CP6u+8SZvcOeAYNG6JbsIrAUUJJ7JfmrnOkR8ipDS+u9SIRQ== dependencies: - archy "^1.0.0" - debug "^4.0.0" - fastq "^1.6.1" + "@fastify/error" "^3.3.0" + fastq "^1.17.1" axios@^1.0.0: version "1.5.0" @@ -3631,7 +3567,7 @@ chai-as-promised@7.1.1: dependencies: check-error "^1.0.2" -chai@4.3.8, chai@^4.3.4, chai@^4.3.7: +chai@4.3.8, chai@^4.3.4: version "4.3.8" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.8.tgz#40c59718ad6928da6629c70496fe990b2bb5b17c" integrity sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ== @@ -3962,10 +3898,10 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cookie@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== core-util-is@~1.0.0: version "1.0.3" @@ -4052,7 +3988,7 @@ dateformat@^4.6.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4267,6 +4203,16 @@ duplexer@^0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +duplexify@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -4523,14 +4469,6 @@ eslint-plugin-prettier@4.2.1: dependencies: prettier-linter-helpers "^1.0.0" -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - eslint-scope@^7.1.1: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" @@ -4634,11 +4572,6 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - estraverse@^5.1.0, estraverse@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" @@ -4666,7 +4599,7 @@ ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" -ethers@5.7.2, ethers@^5.1.0, ethers@^5.6.8, ethers@^5.7.0, ethers@^5.7.2: +ethers@5.7.2, ethers@^5.1.0, ethers@^5.6.8: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -4794,7 +4727,7 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -fast-content-type-parse@^1.0.0: +fast-content-type-parse@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz#4087162bf5af3294d4726ff29b334f72e3a1092c" integrity sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ== @@ -4852,6 +4785,19 @@ fast-json-stringify@^5.7.0: fast-uri "^2.1.0" rfdc "^1.2.0" +fast-json-stringify@^5.8.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-5.16.0.tgz#e35baa9f85a61f81680b2845969f91bd02d1b30e" + integrity sha512-A4bg6E15QrkuVO3f0SwIASgzMzR6XC4qTyTqhf3hYXy0iazbAdZKwkE+ox4WgzKyzM6ygvbdq3r134UjOaaAnA== + dependencies: + "@fastify/merge-json-schemas" "^0.1.0" + ajv "^8.10.0" + ajv-formats "^3.0.1" + fast-deep-equal "^3.1.3" + fast-uri "^2.1.0" + json-schema-ref-resolver "^1.0.1" + rfdc "^1.2.0" + fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -4884,28 +4830,36 @@ fastify-plugin@^4.0.0: resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-4.5.1.tgz#44dc6a3cc2cce0988bc09e13f160120bbd91dbee" integrity sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ== -fastify@4.14.1: - version "4.14.1" - resolved "https://registry.yarnpkg.com/fastify/-/fastify-4.14.1.tgz#be1b27a13910c74ecb8625de4fa42feab9703259" - integrity sha512-yjrDeXe77j9gRlSV2UJry8mcFWbD0NQ5JYjnPi4tkFjHZVaG3/BD5wxOmRzGnHPC0YvaBJ0XWrIfFPl2IHRa1w== +fastify@4.25.2: + version "4.25.2" + resolved "https://registry.yarnpkg.com/fastify/-/fastify-4.25.2.tgz#ce725c9f457149244ebfec848468fa3550f0981f" + integrity sha512-SywRouGleDHvRh054onj+lEZnbC1sBCLkR0UY3oyJwjD4BdZJUrxBqfkfCaqn74pVCwBaRHGuL3nEWeHbHzAfw== dependencies: "@fastify/ajv-compiler" "^3.5.0" - "@fastify/error" "^3.0.0" - "@fastify/fast-json-stringify-compiler" "^4.1.0" + "@fastify/error" "^3.4.0" + "@fastify/fast-json-stringify-compiler" "^4.3.0" abstract-logging "^2.0.1" - avvio "^8.2.0" - fast-content-type-parse "^1.0.0" - find-my-way "^7.3.0" - light-my-request "^5.6.1" - pino "^8.5.0" - process-warning "^2.0.0" + avvio "^8.2.1" + fast-content-type-parse "^1.1.0" + fast-json-stringify "^5.8.0" + find-my-way "^7.7.0" + light-my-request "^5.11.0" + pino "^8.17.0" + process-warning "^3.0.0" proxy-addr "^2.0.7" rfdc "^1.3.0" - secure-json-parse "^2.5.0" - semver "^7.3.7" - tiny-lru "^10.0.0" + secure-json-parse "^2.7.0" + semver "^7.5.4" + toad-cache "^3.3.0" + +fastq@^1.17.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" -fastq@^1.6.0, fastq@^1.6.1: +fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== @@ -4945,10 +4899,10 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-my-way@^7.3.0: - version "7.6.2" - resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-7.6.2.tgz#4dd40200d3536aeef5c7342b10028e04cf79146c" - integrity sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw== +find-my-way@^7.7.0: + version "7.7.0" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-7.7.0.tgz#d7b51ca6046782bcddd5a8435e99ad057e5a8876" + integrity sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ== dependencies: fast-deep-equal "^3.1.3" fast-querystring "^1.0.0" @@ -5373,6 +5327,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + handlebars@^4.7.7: version "4.7.8" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" @@ -5592,6 +5551,11 @@ ignore@^5.0.4, ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +ignore@^5.2.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -6358,6 +6322,13 @@ json-parse-even-better-errors@^3.0.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz#b43d35e89c0f3be6b5fbbe9dc6c82467b30c28da" integrity sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ== +json-schema-ref-resolver@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz#6586f483b76254784fc1d2120f717bdc9f0a99bf" + integrity sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw== + dependencies: + fast-deep-equal "^3.1.3" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -6628,13 +6599,13 @@ libphonenumber-js@^1.10.53: resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.60.tgz#1160ec5b390d46345032aa52be7ffa7a1950214b" integrity sha512-Ctgq2lXUpEJo5j1762NOzl2xo7z7pqmVWYai0p07LvAkQ32tbPv3wb+tcUeHEiXhKU5buM4H9MXsXo6OlM6C2g== -light-my-request@^5.6.1: - version "5.11.0" - resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-5.11.0.tgz#90e446c303b3a47b59df38406d5f5c2cf224f2d1" - integrity sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA== +light-my-request@^5.11.0: + version "5.13.0" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-5.13.0.tgz#b29905e55e8605b77fee2a946e17b219bca35113" + integrity sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ== dependencies: - cookie "^0.5.0" - process-warning "^2.0.0" + cookie "^0.6.0" + process-warning "^3.0.0" set-cookie-parser "^2.4.1" lines-and-columns@^1.1.6: @@ -6963,6 +6934,13 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@9.0.3, minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -6991,13 +6969,6 @@ minimatch@^9.0.0: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -7104,10 +7075,10 @@ mlly@^1.2.0, mlly@^1.4.0: pkg-types "^1.0.3" ufo "^1.3.0" -mnemonist@0.39.5: - version "0.39.5" - resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.5.tgz#5850d9b30d1b2bc57cc8787e5caa40f6c3420477" - integrity sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ== +mnemonist@0.39.6: + version "0.39.6" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.6.tgz#0b3c9b7381d9edf6ce1957e74b25a8ad25732f57" + integrity sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA== dependencies: obliterator "^2.0.1" @@ -7242,11 +7213,6 @@ native-fetch@^4.0.2: resolved "https://registry.yarnpkg.com/native-fetch/-/native-fetch-4.0.2.tgz#75c8a44c5f3bb021713e5e24f2846750883e49af" integrity sha512-4QcVlKFtv2EYVS5MBgsGX5+NWKtbDbIECdUXDBGDMAZXq3Jkv9zf+y8iS7Ub8fEdga3GpYeazp9gauNqXHJOCg== -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -8009,7 +7975,7 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.1.0: +pino-abstract-transport@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz#083d98f966262164504afb989bccd05f665937a8" integrity sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA== @@ -8017,6 +7983,14 @@ pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.1.0: readable-stream "^4.0.0" split2 "^4.0.0" +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + pino-abstract-transport@v1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" @@ -8067,22 +8041,22 @@ pino@8.11.0: sonic-boom "^3.1.0" thread-stream "^2.0.0" -pino@^8.5.0: - version "8.15.1" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.15.1.tgz#04b815ff7aa4e46b1bbab88d8010aaa2b17eaba4" - integrity sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA== +pino@^8.17.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== dependencies: atomic-sleep "^1.0.0" fast-redact "^3.1.1" on-exit-leak-free "^2.1.0" - pino-abstract-transport v1.1.0 + pino-abstract-transport "^1.2.0" pino-std-serializers "^6.0.0" - process-warning "^2.0.0" + process-warning "^3.0.0" quick-format-unescaped "^4.0.3" real-require "^0.2.0" safe-stable-stringify "^2.3.1" - sonic-boom "^3.1.0" - thread-stream "^2.0.0" + sonic-boom "^3.7.0" + thread-stream "^2.6.0" pkg-dir@^4.2.0: version "4.2.0" @@ -8170,16 +8144,22 @@ process-warning@^2.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.2.0.tgz#008ec76b579820a8e5c35d81960525ca64feb626" integrity sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg== +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -prom-client@^14.2.0: - version "14.2.0" - resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-14.2.0.tgz#ca94504e64156f6506574c25fb1c34df7812cf11" - integrity sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA== +prom-client@15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.0.tgz#816a4a2128da169d0471093baeccc6d2f17a4613" + integrity sha512-cCD7jLTqyPdjEPBo/Xk4Iu8jxjuZgZJ3e/oET3L+ZwOuap/7Cw3dH/TJSsZKs1TQLZ2IHpIlRAKw82ef06kmMw== dependencies: + "@opentelemetry/api" "^1.4.0" tdigest "^0.1.1" promise-inflight@^1.0.1: @@ -8688,7 +8668,7 @@ scrypt-js@3.0.1: resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== -secure-json-parse@^2.4.0, secure-json-parse@^2.5.0: +secure-json-parse@^2.4.0, secure-json-parse@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== @@ -8717,7 +8697,7 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semve dependencies: lru-cache "^6.0.0" -semver@^7.3.8: +semver@^7.3.8, semver@^7.5.4: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== @@ -8882,6 +8862,13 @@ sonic-boom@^3.0.0, sonic-boom@^3.1.0: dependencies: atomic-sleep "^1.0.0" +sonic-boom@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== + dependencies: + atomic-sleep "^1.0.0" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" @@ -8985,6 +8972,11 @@ std-env@^3.3.3: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e" integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg== +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + stream-to-it@0.2.4, stream-to-it@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/stream-to-it/-/stream-to-it-0.2.4.tgz#d2fd7bfbd4a899b4c0d6a7e6a533723af5749bd0" @@ -9229,6 +9221,13 @@ thread-stream@^2.0.0: dependencies: real-require "^0.2.0" +thread-stream@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== + dependencies: + real-require "^0.2.0" + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -9254,11 +9253,6 @@ timeout-abort-controller@^3.0.0: dependencies: retimer "^3.0.0" -tiny-lru@^10.0.0: - version "10.4.1" - resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-10.4.1.tgz#dec67a62115a4cb31d2065b8116d010daac362fe" - integrity sha512-buLIzw7ppqymuO3pt10jHk/6QMeZLbidihMQU+N6sogF6EnBzG0qtDWIHuhw1x3dyNgVL/KTGIZsTK81+yCzLg== - "tiny-worker@>= 2": version "2.3.0" resolved "https://registry.yarnpkg.com/tiny-worker/-/tiny-worker-2.3.0.tgz#715ae34304c757a9af573ae9a8e3967177e6011e" @@ -9302,6 +9296,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toad-cache@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/toad-cache/-/toad-cache-3.7.0.tgz#b9b63304ea7c45ec34d91f1d2fa513517025c441" + integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw== + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -9324,6 +9323,11 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" +ts-api-utils@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + ts-node@10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -9371,23 +9375,11 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - tuf-js@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/tuf-js/-/tuf-js-1.1.7.tgz#21b7ae92a9373015be77dfe0cb282a80ec3bbe43" @@ -9483,12 +9475,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@4.8.4: - version "4.8.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== - -"typescript@>=3 < 6": +typescript@5.4.5, "typescript@>=3 < 6": version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== @@ -9906,6 +9893,16 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + +ws@^8.0.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== + xml2js@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.5.0.tgz#d9440631fbb2ed800203fad106f2724f62c493b7"