diff --git a/.eslintrc.js b/.eslintrc.js index 13b79a6a..e31835a6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -71,6 +71,7 @@ module.exports = { allowAny: true, }, ], + "@typescript-eslint/no-explicit-any": "off", "import/no-extraneous-dependencies": [ "error", { @@ -161,8 +162,8 @@ module.exports = { }, { files: ["**/test/**/*.test.ts"], - plugins: ["mocha", "chai-expect"], - extends: ["plugin:mocha/recommended", "plugin:chai-expect/recommended"], + plugins: ["mocha"], + extends: ["plugin:mocha/recommended"], rules: { // We observed that having multiple top level "describe" save valuable indentation // https://github.com/lo1tuma/eslint-plugin-mocha/blob/master/docs/rules/max-top-level-suites.md diff --git a/README.md b/README.md index 56ef6b1a..33d788b2 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) @@ -58,11 +58,14 @@ For a video tutorial on the above, you can [view this here.](https://www.youtube - [x] Unsafe mode - bypass opcode & stake validation - [x] Redirect RPC - Redirect ETH rpc calls to the underlying execution client. This is needed if you use UserOp.js - [x] P2P - Exchange of UserOps between all the nodes in the network. Heavily inspired by the Lodestar's implementation of p2p (https://github.com/ChainSafe/lodestar/) +- [x] Websockets event - to listen to pending and submitted userops ### ⚡️ CLI Options - `--unsafeMode` - enables unsafeMode - `--redirectRpc` - enables redirecting eth rpc calls - `--executor.bundlingMode manual|auto` - sets bundling mode to `manual` or `auto` on start. Default value is `auto` +- `--api.ws true|false` - enables / disables websocket server. Default is `true` +- `--api.wsPort number` - sets websocket service port. Default is the same as `api.port` ## 🔑 Relayer Configuration @@ -124,6 +127,7 @@ For a video tutorial on the above, you can [view this here.](https://www.youtube "skipBundleValidation": false, # # optional, skips bundle validation "userOpGasLimit": 25000000, # optional, gas limit of a userop "bundleGasLimit": 25000000, # optional, gas limit of a bundle + "archiveDuration": 5184000 # optional, keeps submitted, reverted and cancelled userops in the mempool for this many seconds } ``` ## 💬 Contact diff --git a/P2P.md b/docs/p2p.md similarity index 96% rename from P2P.md rename to docs/p2p.md index 58188d65..9a786e2a 100644 --- a/P2P.md +++ b/docs/p2p.md @@ -1,68 +1,68 @@ -# How to test p2p - -### Run geth-dev - -`cd test` -`docker-compose up -d geth-dev` - -### Deploy EP and Factory from EF account-abstraction repo - -1. clone the https://github.com/eth-infinitism/account-abstraction repo `git clone https://github.com/eth-infinitism/account-abstraction.git` -2. run `yarn deploy --network localhost` - -We should have deployments in the following addresses. -- Entrypoint addr: 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 -- SimpleAccountFactory addr: 0x9406Cc6185a346906296840746125a0E44976454 -- SimpleAccount addr: 0xbba97eC4fFF328d485382DfD5A9bf9653c6018Af // sample address, deployed address could be different - -### Top up account - -go to docker console -``` -> geth attach http://127.0.0.1:8545 -``` -Inside geth terminal -``` -> eth.sendTransaction({ from: eth.accounts[0], to: "0xbba97eC4fFF328d485382DfD5A9bf9653c6018Af", value: 1000000000000000000 }) -``` - -### Modify the - -### Generate userop from erc4337 examples - -`yarn simpleAccount transfer --to 0x9406Cc6185a346906296840746125a0E44976454 --amount 0` - -Example: -``` -{ - "sender":"0xbba97eC4fFF328d485382DfD5A9bf9653c6018Af", - "nonce":"0x0", - "initCode":"0x9406cc6185a346906296840746125a0e449764545fbfb9cf00000000000000000000000005449b55b91e9ebdd099ed584cb6357234f2ab3b0000000000000000000000000000000000000000000000000000000000000000", - "callData":"0xb61d27f60000000000000000000000009406cc6185a346906296840746125a0e4497645400000000000000000000000000000000000000000000000000000000000f424000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", - "callGasLimit":"0x5568", - "verificationGasLimit":"0x5ea0c", - "preVerificationGas":"0xb820", - "maxFeePerGas":"0x4ac312de", - "maxPriorityFeePerGas":"0xed2ba9a", - "paymasterAndData":"0x", - "signature":"0xecdc2665d72b04bf133e39a3849d3eedc4913550d17f839eda442db2ea175e906750b860ba0e3ac1d3de068c2e16183a1e430f77fae2ec94e44298083576033e1c" -} -``` - -### Run the bootnode - -``` -./skandha node --redirectRpc --executor.bundlingMode manual -``` - -### Run a regular node - -``` -./skandha node --redirectRpc --executor.bundlingMode manual --dataDir ./db --api.port 14338 --p2p.port 4338 --p2p.enrPort 4338 --p2p.bootEnrs [enr] -``` - -### Run the second regular node - -``` -./skandha node --redirectRpc --executor.bundlingMode manual --dataDir ./db2 --api.port 14339 --p2p.port 4339 --p2p.enrPort 4339 --p2p.bootEnrs [enr] -``` +## How to test p2p + +### Run geth-dev + +`cd test` +`docker-compose up -d geth-dev` + +### Deploy EP and Factory from EF account-abstraction repo + +1. clone the https://github.com/eth-infinitism/account-abstraction repo `git clone https://github.com/eth-infinitism/account-abstraction.git` +2. run `yarn deploy --network localhost` + +We should have deployments in the following addresses. +- Entrypoint addr: 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 +- SimpleAccountFactory addr: 0x9406Cc6185a346906296840746125a0E44976454 +- SimpleAccount addr: 0xbba97eC4fFF328d485382DfD5A9bf9653c6018Af // sample address, deployed address could be different + +### Top up account + +go to docker console +``` +> geth attach http://127.0.0.1:8545 +``` +Inside geth terminal +``` +> eth.sendTransaction({ from: eth.accounts[0], to: "0xbba97eC4fFF328d485382DfD5A9bf9653c6018Af", value: 1000000000000000000 }) +``` + +### Modify the + +### Generate userop from erc4337 examples + +`yarn simpleAccount transfer --to 0x9406Cc6185a346906296840746125a0E44976454 --amount 0` + +Example: +``` +{ + "sender":"0xbba97eC4fFF328d485382DfD5A9bf9653c6018Af", + "nonce":"0x0", + "initCode":"0x9406cc6185a346906296840746125a0e449764545fbfb9cf00000000000000000000000005449b55b91e9ebdd099ed584cb6357234f2ab3b0000000000000000000000000000000000000000000000000000000000000000", + "callData":"0xb61d27f60000000000000000000000009406cc6185a346906296840746125a0e4497645400000000000000000000000000000000000000000000000000000000000f424000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit":"0x5568", + "verificationGasLimit":"0x5ea0c", + "preVerificationGas":"0xb820", + "maxFeePerGas":"0x4ac312de", + "maxPriorityFeePerGas":"0xed2ba9a", + "paymasterAndData":"0x", + "signature":"0xecdc2665d72b04bf133e39a3849d3eedc4913550d17f839eda442db2ea175e906750b860ba0e3ac1d3de068c2e16183a1e430f77fae2ec94e44298083576033e1c" +} +``` + +### Run the bootnode + +``` +./skandha node --redirectRpc --executor.bundlingMode manual +``` + +### Run a regular node + +``` +./skandha node --redirectRpc --executor.bundlingMode manual --dataDir ./db --api.port 14338 --p2p.port 4338 --p2p.enrPort 4338 --p2p.bootEnrs [enr] +``` + +### Run the second regular node + +``` +./skandha node --redirectRpc --executor.bundlingMode manual --dataDir ./db2 --api.port 14339 --p2p.port 4339 --p2p.enrPort 4339 --p2p.bootEnrs [enr] +``` 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..fe878609 --- /dev/null +++ b/docs/skandha_userOperationStatus.md @@ -0,0 +1,99 @@ +## 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", + "id": 3, + "result": { + "userOp": { + "sender": "0xb582979C2136189475326c648732F76677B16B98", + "nonce": "0x4", + "initCode": "0x", + "callData": "0x47e1da2a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000009fd4f6088f2025427ab1e89257a44747081ed590000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xb957", + "verificationGasLimit": "0x9b32", + "maxFeePerGas": "0x1cbbd765c", + "maxPriorityFeePerGas": "0x59682f00", + "paymasterAndData": "0x", + "preVerificationGas": "0xae70", + "signature": "0xb4ff57fa73e803b4e20b9016af228d30fd1563e71ce8313724230c5883560c873cc5785264e172d11ded202b4f4fc7eaaf88dee57965546f1747ebb5054178d51b" + }, + "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "status": "New" + } +} +``` + +#### Response (after some time) + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "userOp": { + "sender": "0xb582979C2136189475326c648732F76677B16B98", + "nonce": "0x4", + "initCode": "0x", + "callData": "0x47e1da2a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000009fd4f6088f2025427ab1e89257a44747081ed590000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xb957", + "verificationGasLimit": "0x9b32", + "maxFeePerGas": "0x1cbbd765c", + "maxPriorityFeePerGas": "0x59682f00", + "paymasterAndData": "0x", + "preVerificationGas": "0xae70", + "signature": "0xb4ff57fa73e803b4e20b9016af228d30fd1563e71ce8313724230c5883560c873cc5785264e172d11ded202b4f4fc7eaaf88dee57965546f1747ebb5054178d51b" + }, + "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "status": "Submitted", + "transaction": "0x0d6ea9c97aa783894dad00d31fc80f7615f49b3a5a8a3dca556d7ca595dcc4e1" + } +} +``` + +#### Response (after appearing on chain) + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "userOp": { + "sender": "0xb582979C2136189475326c648732F76677B16B98", + "nonce": "0x4", + "initCode": "0x", + "callData": "0x47e1da2a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000009fd4f6088f2025427ab1e89257a44747081ed590000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0xb957", + "verificationGasLimit": "0x9b32", + "maxFeePerGas": "0x1cbbd765c", + "maxPriorityFeePerGas": "0x59682f00", + "paymasterAndData": "0x", + "preVerificationGas": "0xae70", + "signature": "0xb4ff57fa73e803b4e20b9016af228d30fd1563e71ce8313724230c5883560c873cc5785264e172d11ded202b4f4fc7eaaf88dee57965546f1747ebb5054178d51b" + }, + "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "status": "OnChain", + "transaction": "0x0d6ea9c97aa783894dad00d31fc80f7615f49b3a5a8a3dca556d7ca595dcc4e1" + } +} +``` \ No newline at end of file diff --git a/lerna.json b/lerna.json index fa029f8b..c969a5fd 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,7 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "1.5.4", + "version": "1.5.5", "stream": "true", "command": { "version": { diff --git a/package.json b/package.json index cec08490..d6549524 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "root", "private": true, - "version": "1.5.4", + "version": "1.5.5", "engines": { "node": ">=18.0.0" }, @@ -32,8 +32,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", @@ -42,7 +42,7 @@ "lerna": "6.4.1", "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 d4cdabcc..54cd1252 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "api", - "version": "1.5.4", + "version": "1.5.5", "description": "The API module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -28,19 +28,22 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@fastify/cors": "8.2.1", + "@fastify/cors": "9.0.1", + "@fastify/websocket": "10.0.1", "class-transformer": "0.5.1", "class-validator": "0.14.1", "ethers": "5.7.2", - "executor": "^1.5.4", - "fastify": "4.14.1", - "monitoring": "^1.5.4", + "executor": "^1.5.5", + "fastify": "4.25.2", + "monitoring": "^1.5.5", "pino": "8.11.0", "pino-pretty": "10.0.0", "reflect-metadata": "0.1.13", - "types": "^1.5.4" + "types": "^1.5.5", + "utils": "^1.5.5", + "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 93e2a4d4..2e27f4c9 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -1,24 +1,32 @@ +import { WebSocket } from "ws"; import { Executor } from "executor/lib/executor"; import { Config } from "executor/lib/config"; import RpcError from "types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; -import { FastifyInstance, RouteHandler } from "fastify"; +import { deepHexlify } from "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,183 +42,280 @@ 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, - auth: string | undefined - ): Promise => { - let result: any; - const { method, params, jsonrpc, id } = request; - // ADMIN METHODS - if ( - this.testingMode || - ip === "localhost" || - ip === "127.0.0.1" || - (process.env.SKANDHA_ADMIN_KEY && - auth === process.env.SKANDHA_ADMIN_KEY) - ) { - 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_dumpMempoolRaw: - result = await debugApi.dumpMempoolRaw(/* 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; + } + default: { + return false; // the request can not be handled by this function } - return { jsonrpc, id, ...body }; } + } 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_getGasPrice: - result = await skandhaApi.getGasPrice(); - break; - case CustomRPCMethods.skandha_feeHistory: - result = await skandhaApi.getFeeHistory({ - entryPoint: params[0], - blockCount: params[1], - newestBlock: params[2], + } else { + result = await this.ethApi.estimateUserOperationGas({ + userOp: params[0], + entryPoint: params[1], }); - break; - case CustomRPCMethods.skandha_config: - result = await skandhaApi.getConfig(); - // skip hexlify for this particular rpc - return { jsonrpc, id, result }; - case CustomRPCMethods.skandha_peers: - result = await skandhaApi.getPeers(); - break; - default: - throw new RpcError( - `Method ${method} is not supported`, - RpcErrorCodes.METHOD_NOT_FOUND - ); + } + break; } - } - - 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, req.headers.authorization) + 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_peers: + result = await this.skandhaApi.getPeers(); + break; + 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 ); - } - } else { - response = await handleRpc(req.ip, req.body, req.headers.authorization); } - 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 e5323d39..6fa17294 100644 --- a/packages/api/src/constants.ts +++ b/packages/api/src/constants.ts @@ -3,6 +3,9 @@ export const CustomRPCMethods = { skandha_config: "skandha_config", skandha_feeHistory: "skandha_feeHistory", skandha_peers: "skandha_peers", + skandha_userOperationStatus: "skandha_userOperationStatus", + skandha_subscribe: "skandha_subscribe", + skandha_unsubscribe: "skandha_unsubscribe", }; export const BundlerRPCMethods = { diff --git a/packages/api/src/dto/EstimateUserOperation.dto.ts b/packages/api/src/dto/EstimateUserOperation.dto.ts index ba6fa3d5..1a98685b 100644 --- a/packages/api/src/dto/EstimateUserOperation.dto.ts +++ b/packages/api/src/dto/EstimateUserOperation.dto.ts @@ -8,8 +8,7 @@ import { } from "class-validator"; import { BigNumberish, BytesLike } from "ethers"; import { Type } from "class-transformer"; -import { IsBigNumber } from "../utils/is-bignumber"; -import { IsCallData } from "../utils/IsCallCode"; +import { IsBigNumber, IsCallData } from "../utils"; export class EstimateUserOperationStruct { @IsEthereumAddress() 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 6b359a8a..f156a141 100644 --- a/packages/api/src/dto/SendUserOperation.dto.ts +++ b/packages/api/src/dto/SendUserOperation.dto.ts @@ -7,8 +7,7 @@ import { } from "class-validator"; import { BigNumberish, BytesLike } from "ethers"; import { Type } from "class-transformer"; -import { IsBigNumber } from "../utils/is-bignumber"; -import { IsCallData } from "../utils/IsCallCode"; +import { IsBigNumber, IsCallData } from "../utils"; export class SendUserOperationStruct { @IsEthereumAddress() diff --git a/packages/api/src/interface.ts b/packages/api/src/interface.ts new file mode 100644 index 00000000..8051b8a6 --- /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/index.ts b/packages/api/src/modules/index.ts index c1426aa7..af133329 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 e3b39c2d..5ceeb25c 100644 --- a/packages/api/src/modules/skandha.ts +++ b/packages/api/src/modules/skandha.ts @@ -3,6 +3,7 @@ import { GetConfigResponse, GetFeeHistoryResponse, GetGasPriceResponse, + UserOperationStatus, } from "types/lib/api/interfaces"; import { Skandha } from "executor/lib/modules"; import RpcError from "types/lib/api/errors/rpc-error"; @@ -31,6 +32,14 @@ export class SkandhaAPI { ); } + /** + * @params hash hash of a userop + * @returns status + */ + async getUserOperationStatus(hash: string): Promise { + return this.skandhaModule.getUserOperationStatus(hash); + } + async getGasPrice(): Promise { return await this.skandhaModule.getGasPrice(); } @@ -39,6 +48,7 @@ export class SkandhaAPI { return await this.skandhaModule.getConfig(); } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type async getPeers() { return await this.skandhaModule.getPeers(); } diff --git a/packages/api/src/modules/subscription.ts b/packages/api/src/modules/subscription.ts new file mode 100644 index 00000000..4d21860e --- /dev/null +++ b/packages/api/src/modules/subscription.ts @@ -0,0 +1,35 @@ +import { WebSocket } from "ws"; +import { SubscriptionService, ExecutorEvent } from "executor/lib/services"; +import RpcError from "types/lib/api/errors/rpc-error"; +import * as RpcErrorCodes from "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 dea6c8ff..4b10356e 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 "types/lib/api/errors/rpc-error"; import { ServerConfig } from "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/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/api/src/utils/RpcMethodValidator.ts b/packages/api/src/utils/RpcMethodValidator.ts index 3970fe8b..8e6592ed 100644 --- a/packages/api/src/utils/RpcMethodValidator.ts +++ b/packages/api/src/utils/RpcMethodValidator.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import "reflect-metadata"; import { validate } from "class-validator"; import { plainToInstance } from "class-transformer"; diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 30e36676..c6af3e00 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/cli/.git-data.json b/packages/cli/.git-data.json new file mode 100644 index 00000000..acfe00bd --- /dev/null +++ b/packages/cli/.git-data.json @@ -0,0 +1,4 @@ +{ + "branch": "websocket", + "commit": "c282c10746adb418c4006d5179b019619f32d2fa" +} \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index 4439e7ba..6b877b04 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "cli", - "version": "1.5.4", + "version": "1.5.5", "description": "> TODO: description", "author": "zincoshine ", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -36,15 +36,15 @@ "@libp2p/peer-id-factory": "2.0.1", "@libp2p/prometheus-metrics": "1.1.3", "@multiformats/multiaddr": "12.1.3", - "api": "^1.5.4", - "db": "^1.5.4", - "executor": "^1.5.4", + "api": "^1.5.5", + "db": "^1.5.5", + "executor": "^1.5.5", "find-up": "5.0.0", "got": "12.5.3", "js-yaml": "4.1.0", - "monitoring": "^1.5.4", - "node": "^1.5.4", - "types": "^1.5.4", + "monitoring": "^1.5.5", + "node": "^1.5.5", + "types": "^1.5.5", "yargs": "17.6.2" }, "devDependencies": { diff --git a/packages/cli/src/cmds/node/handler.ts b/packages/cli/src/cmds/node/handler.ts index a9c347d2..801d64f3 100644 --- a/packages/cli/src/cmds/node/handler.ts +++ b/packages/cli/src/cmds/node/handler.ts @@ -67,6 +67,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, networkDataDir), }; @@ -120,6 +122,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 03618d6d..a807a053 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,7 +125,7 @@ export async function bundlerHandler( : null; new ApiApp({ - server: server.application, + server: server, config: config, testingMode, redirectRpc, diff --git a/packages/cli/src/options/bundlerOptions/api.ts b/packages/cli/src/options/bundlerOptions/api.ts index 2fb9d919..7db47bee 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/contracts/package.json b/packages/contracts/package.json index 7845d954..70efbc9f 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "contracts", - "version": "1.5.4", + "version": "1.5.5", "description": "Smart contracts of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", diff --git a/packages/db/package.json b/packages/db/package.json index 205a8632..9945cb51 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "db", - "version": "1.5.4", + "version": "1.5.5", "description": "The DB module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://github.com/etherspot/etherspot-bundler#readme", @@ -30,7 +30,7 @@ "dependencies": { "@chainsafe/ssz": "0.10.1", "@farcaster/rocksdb": "5.5.0", - "types": "^1.5.4" + "types": "^1.5.5" }, "devDependencies": { "@types/rocksdb": "3.0.1", diff --git a/packages/executor/package.json b/packages/executor/package.json index f24bf92a..7128ee40 100644 --- a/packages/executor/package.json +++ b/packages/executor/package.json @@ -1,6 +1,6 @@ { "name": "executor", - "version": "1.5.4", + "version": "1.5.5", "description": "The Relayer module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -33,8 +33,14 @@ "@flashbots/ethers-provider-bundle": "0.6.2", "async-mutex": "0.4.0", "ethers": "5.7.2", - "monitoring": "^1.5.4", - "params": "^1.5.4", - "types": "^1.5.4" + "monitoring": "^1.5.5", + "params": "^1.5.5", + "strict-event-emitter-types": "2.0.0", + "types": "^1.5.5", + "utils": "^1.5.5", + "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 6cc181d4..0b6ebaa5 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; } @@ -150,11 +156,9 @@ export class Config { ); config.minStake = BigNumber.from( - fromEnvVar( - "MIN_STAKE", - config.minStake ?? bundlerDefaultConfigs.minStake - ) + fromEnvVar("MIN_STAKE", config.minStake ?? bundlerDefaultConfigs.minStake) ); + config.minUnstakeDelay = Number( fromEnvVar( "MIN_UNSTAKE_DELAY", @@ -237,10 +241,7 @@ export class Config { ); config.banSlack = Number( - fromEnvVar( - "BAN_SLACK", - config.banSlack || bundlerDefaultConfigs.banSlack - ) + fromEnvVar("BAN_SLACK", config.banSlack || bundlerDefaultConfigs.banSlack) ); config.minInclusionDenominator = Number( @@ -261,7 +262,8 @@ export class Config { config.skipBundleValidation = Boolean( fromEnvVar( "SKIP_BUNDLE_VALIDATION", - config.skipBundleValidation || bundlerDefaultConfigs.skipBundleValidation + config.skipBundleValidation || + bundlerDefaultConfigs.skipBundleValidation ) ); @@ -293,6 +295,12 @@ export class Config { ) ); + config.fastlaneValidators = fromEnvVar( + "FASTLANE_VALIDATOR", + config.fastlaneValidators || bundlerDefaultConfigs.fastlaneValidators, + true + ) as string[]; + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!config.whitelistedEntities) { config.whitelistedEntities = bundlerDefaultConfigs.whitelistedEntities; @@ -360,6 +368,8 @@ const bundlerDefaultConfigs: BundlerConfig = { kolibriAuthKey: "", entryPointForwarder: "", echoAuthKey: "", + archiveDuration: 24 * 3600, + fastlaneValidators: [], }; 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 886379d7..a5b56d87 100644 --- a/packages/executor/src/entities/MempoolEntry.ts +++ b/packages/executor/src/entities/MempoolEntry.ts @@ -19,9 +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 + transaction?: string; // transaction hash of a submitted bundle + actualTransaction?: string; // hash of an actual transaction (in case the original tx was front-runned) submitAttempts: number; submittedTime?: number; // timestamp when mempool was first put into the mempool + revertReason?: string; constructor({ chainId, @@ -36,8 +38,10 @@ export class MempoolEntry implements IMempoolEntry { lastUpdatedTime, status, transaction, + actualTransaction, submitAttempts, submittedTime, + revertReason, }: { chainId: number; userOp: UserOperationStruct; @@ -51,6 +55,8 @@ export class MempoolEntry implements IMempoolEntry { lastUpdatedTime?: number | undefined; status?: MempoolEntryStatus | undefined; transaction?: string | undefined; + actualTransaction?: string | undefined; + revertReason?: string | undefined; submitAttempts?: number | undefined; submittedTime?: number | undefined; }) { @@ -68,6 +74,8 @@ export class MempoolEntry implements IMempoolEntry { this.status = status ?? MempoolEntryStatus.New; this.transaction = transaction; this.submitAttempts = submitAttempts ?? 0; + this.actualTransaction = actualTransaction; + this.revertReason = revertReason; this.validateAndTransformUserOp(); } @@ -75,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 + } } } @@ -191,6 +224,8 @@ export class MempoolEntry implements IMempoolEntry { 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 6a759514..12fcd53b 100644 --- a/packages/executor/src/entities/interfaces.ts +++ b/packages/executor/src/entities/interfaces.ts @@ -17,6 +17,8 @@ export interface IMempoolEntry { transaction?: string; submitAttempts: number; submittedTime?: number; + actualTransaction?: string; + revertReason?: string; } export interface MempoolEntrySerialized { @@ -45,6 +47,8 @@ export interface MempoolEntrySerialized { submitAttempts: number; status: MempoolEntryStatus; submittedTime: number | undefined; + actualTransaction: string | undefined; + revertReason: string | undefined; } export interface IReputationEntry { diff --git a/packages/executor/src/executor.ts b/packages/executor/src/executor.ts index d64132a9..d3b41635 100644 --- a/packages/executor/src/executor.ts +++ b/packages/executor/src/executor.ts @@ -12,6 +12,8 @@ import { ReputationService, P2PService, EventsService, + ExecutorEventBus, + SubscriptionService, } from "./services"; import { Config } from "./config"; import { BundlingMode, GetNodeAPI, NetworkConfig } from "./interfaces"; @@ -47,7 +49,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 +73,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, @@ -76,12 +89,22 @@ export class Executor { this.networkConfig.minUnstakeDelay ); + this.mempoolService = new MempoolService( + this.db, + this.chainId, + this.reputationService, + this.eventBus, + this.networkConfig, + this.logger + ); + this.skandha = new Skandha( this.getNodeApi, + this.mempoolService, this.chainId, this.provider, this.config, - this.logger, + this.logger ); this.userOpValidationService = new UserOpValidationService( @@ -92,19 +115,13 @@ export class Executor { this.config, this.logger ); - this.mempoolService = new MempoolService( - this.db, - this.chainId, - this.reputationService, - this.networkConfig, - this.logger - ); this.bundlingService = new BundlingService( this.chainId, this.provider, this.mempoolService, this.userOpValidationService, this.reputationService, + this.eventBus, this.config, this.logger, this.metrics, @@ -116,6 +133,7 @@ export class Executor { this.logger, this.reputationService, this.mempoolService, + this.eventBus, this.networkConfig.entryPoints, this.db ); @@ -181,5 +199,9 @@ export class Executor { } this.logger.info(`[x] USEROPS TTL - ${this.networkConfig.useropsTTL}`); + + setInterval(() => { + this.subscriptionService.onPing(); + }, 3000); } } diff --git a/packages/executor/src/interfaces.ts b/packages/executor/src/interfaces.ts index 3d9e228f..11045d6e 100644 --- a/packages/executor/src/interfaces.ts +++ b/packages/executor/src/interfaces.ts @@ -164,6 +164,10 @@ export interface NetworkConfig { entryPointForwarder: string; // api auth key for echo: https://echo.chainbound.io/docs/usage/api-interface#authentication echoAuthKey: string; + // keep submitted, reverted and cancelled userops in the mempool for this many seconds + // default: 24 hours + archiveDuration: number; + fastlaneValidators: string[]; } export type BundlerConfig = Omit< diff --git a/packages/executor/src/modules/eth.ts b/packages/executor/src/modules/eth.ts index 87b5ce1a..b87ed3ca 100644 --- a/packages/executor/src/modules/eth.ts +++ b/packages/executor/src/modules/eth.ts @@ -12,6 +12,7 @@ import { UserOperationByHashResponse, UserOperationReceipt, } from "types/lib/api/interfaces"; +import { MempoolEntryStatus } from "types/lib/executor"; import { IEntryPoint__factory } from "types/lib/executor/contracts/factories"; import { IPVGEstimator } from "params/lib/types/IPVGEstimator"; import { @@ -22,7 +23,8 @@ import { } from "params/lib"; import { Logger } from "types/lib"; import { PerChainMetrics } from "monitoring/lib"; -import { deepHexlify, packUserOp } from "../utils"; +import { deepHexlify } from "utils/lib/hexlify"; +import { packUserOp } from "../utils"; import { UserOpValidationService, MempoolService } from "../services"; import { GetNodeAPI, Log, NetworkConfig } from "../interfaces"; import { getUserOpGasLimit } from "../services/BundlingService/utils"; @@ -114,7 +116,10 @@ export class Eth { const nodeApi = this.getNodeAPI(); if (nodeApi) { const { canonicalEntryPoint, canonicalMempoolId } = this.config; - if (canonicalEntryPoint.toLowerCase() == entryPoint.toLowerCase() && canonicalMempoolId.length > 0) { + if ( + canonicalEntryPoint.toLowerCase() == entryPoint.toLowerCase() && + canonicalMempoolId.length > 0 + ) { const blockNumber = await this.provider.getBlockNumber(); // TODO: fetch blockNumber from simulateValidation await nodeApi.publishVerifiedUserOperationJSON( entryPoint, @@ -310,7 +315,7 @@ export class Eth { hash: string ): Promise { const entry = await this.mempoolService.getEntryByHash(hash); - if (entry) { + if (entry && entry.status < MempoolEntryStatus.Submitted) { let transaction: Partial = {}; if (entry.transaction) { transaction = await this.provider.getTransaction(entry.transaction); @@ -430,7 +435,7 @@ export class Eth { validateEntryPoint(entryPoint: string): boolean { return ( - (this.config.entryPoints as any) && + this.config.entryPoints != null && this.config.entryPoints.findIndex( (ep) => ep.toLowerCase() === entryPoint.toLowerCase() ) !== -1 @@ -463,6 +468,7 @@ export class Eth { preVerificationGas: 21000, signature: hexlify(Buffer.alloc(ov.sigSize, 1)), ...userOp, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; const packed = arrayify(packUserOp(p, false)); diff --git a/packages/executor/src/modules/skandha.ts b/packages/executor/src/modules/skandha.ts index 93e36a70..7c6480ee 100644 --- a/packages/executor/src/modules/skandha.ts +++ b/packages/executor/src/modules/skandha.ts @@ -4,6 +4,7 @@ import { GetConfigResponse, GetFeeHistoryResponse, GetGasPriceResponse, + UserOperationStatus, } from "types/lib/api/interfaces"; import RpcError from "types/lib/api/errors/rpc-error"; import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; @@ -11,8 +12,10 @@ import { GasPriceMarkupOne } from "params/lib"; import { getGasFee } from "params/lib"; import { IEntryPoint__factory } from "types/lib/executor/contracts"; import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint"; +import { MempoolEntryStatus } from "types/lib/executor"; import { GetNodeAPI, NetworkConfig } from "../interfaces"; import { Config } from "../config"; +import { MempoolService } from "../services"; // custom features of Skandha export class Skandha { @@ -20,6 +23,7 @@ export class Skandha { constructor( private getNodeAPI: GetNodeAPI = () => null, + private mempoolService: MempoolService, private chainId: number, private provider: ethers.providers.JsonRpcProvider, private config: Config, @@ -30,6 +34,34 @@ export class Skandha { void this.getConfig().then((config) => this.logger.debug(config)); } + 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, + }; + } + async getGasPrice(): Promise { const multiplier = this.networkConfig.gasPriceMarkup; const gasFee = await getGasFee( @@ -96,9 +128,7 @@ export class Skandha { minSignerBalance: `${ethers.utils.formatEther( this.networkConfig.minSignerBalance )} eth`, - minStake: `${ethers.utils.formatEther( - this.networkConfig.minStake! - )} eth`, + minStake: `${ethers.utils.formatEther(this.networkConfig.minStake!)} eth`, multicall: this.networkConfig.multicall, validationGasLimit: BigNumber.from( this.networkConfig.validationGasLimit @@ -133,6 +163,8 @@ export class Skandha { gasFeeInSimulation: this.networkConfig.gasFeeInSimulation, userOpGasLimit: this.networkConfig.userOpGasLimit, bundleGasLimit: this.networkConfig.bundleGasLimit, + archiveDuration: this.networkConfig.archiveDuration, + fastlaneValidators: this.networkConfig.fastlaneValidators, }; } @@ -191,6 +223,7 @@ export class Skandha { }; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type async getPeers() { const nodeApi = this.getNodeAPI(); if (!nodeApi) return []; diff --git a/packages/executor/src/services/BundlingService/interfaces.ts b/packages/executor/src/services/BundlingService/interfaces.ts index 10cd560d..f2d4c65a 100644 --- a/packages/executor/src/services/BundlingService/interfaces.ts +++ b/packages/executor/src/services/BundlingService/interfaces.ts @@ -7,4 +7,5 @@ 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 beba03d9..76bec3b1 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 "types/lib"; import { PerChainMetrics } from "monitoring/lib"; +import { MempoolEntryStatus } from "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 { getAddr, now } from "../../../utils"; import { MempoolService } from "../../MempoolService"; import { ReputationService } from "../../ReputationService"; +import { ExecutorEventBus } from "../../SubscriptionService"; const WAIT_FOR_TX_MAX_RETRIES = 3; // 3 blocks @@ -24,6 +26,7 @@ export abstract class BaseRelayer implements IRelayingMode { protected networkConfig: NetworkConfig, protected mempoolService: MempoolService, protected reputationService: ReputationService, + protected eventBus: ExecutorEventBus, protected metrics: PerChainMetrics | null ) { const relayers = this.config.getRelayers(); @@ -44,6 +47,10 @@ export abstract class BaseRelayer implements IRelayingMode { return this.mutexes.filter((mutex) => !mutex.isLocked()).length; } + async canSubmitBundle(): Promise { + return true; + } + /** * waits entries to get submitted * @param hashes user op hashes array @@ -53,7 +60,10 @@ export abstract class BaseRelayer implements IRelayingMode { if (entries.length == 0) return; return new Promise((resolve, reject) => { const interval = setInterval(async () => { - if (retries >= WAIT_FOR_TX_MAX_RETRIES) reject(false); + if (retries >= WAIT_FOR_TX_MAX_RETRIES) { + clearInterval(interval); + reject(false); + } retries++; for (const entry of entries) { const exists = await this.mempoolService.find(entry); @@ -103,7 +113,13 @@ export abstract class BaseRelayer implements IRelayingMode { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (failedEntry) { this.logger.debug(`${failedEntry.hash} reverted on chain. Deleting...`); - await this.mempoolService.remove(failedEntry); + await this.mempoolService.updateStatus( + [failedEntry], + MempoolEntryStatus.Reverted, + { + revertReason: reason, + } + ); this.logger.error( `Failed handleOps sender=${failedEntry.userOp.sender}` ); @@ -155,4 +171,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 511a6bec..57f75ee8 100644 --- a/packages/executor/src/services/BundlingService/relayers/classic.ts +++ b/packages/executor/src/services/BundlingService/relayers/classic.ts @@ -1,41 +1,13 @@ import { providers } from "ethers"; -import { Logger } from "types/lib"; -import { PerChainMetrics } from "monitoring/lib"; import { IEntryPoint__factory } from "types/lib/executor/contracts"; import { chainsWithoutEIP1559 } from "params/lib"; import { AccessList } from "ethers/lib/utils"; -import { MempoolEntryStatus } from "types/lib/executor"; import { Relayer } from "../interfaces"; -import { Config } from "../../../config"; -import { Bundle, NetworkConfig, StorageMap } from "../../../interfaces"; -import { MempoolService } from "../../MempoolService"; +import { Bundle, StorageMap } from "../../../interfaces"; import { estimateBundleGasLimit } from "../utils"; -import { ReputationService } from "../../ReputationService"; import { BaseRelayer } from "./base"; export class ClassicRelayer extends BaseRelayer { - constructor( - logger: Logger, - chainId: number, - provider: providers.JsonRpcProvider, - config: Config, - networkConfig: NetworkConfig, - mempoolService: MempoolService, - reputationService: ReputationService, - metrics: PerChainMetrics | null - ) { - super( - logger, - chainId, - provider, - config, - networkConfig, - mempoolService, - reputationService, - metrics - ); - } - async sendBundle(bundle: Bundle): Promise { const availableIndex = this.getAvailableRelayerIndex(); if (availableIndex == null) { @@ -114,23 +86,10 @@ export class ClassicRelayer extends BaseRelayer { if (!this.config.testingMode) { // check for execution revert - if (!this.networkConfig.skipBundleValidation) { - 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.debug( - `${entries - .map((entry) => entry.userOpHash) - .join("; ")} failed on chain estimation. deleting...` - ); - this.logger.error(err); - await this.mempoolService.removeAll(entries); - this.reportFailedBundle(); - return; - } + if ( + !(await this.validateBundle(relayer, entries, transactionRequest)) + ) { + return; } this.logger.debug( @@ -144,11 +103,7 @@ export class ClassicRelayer extends BaseRelayer { this.logger.debug( `User op hashes ${entries.map((entry) => entry.userOpHash)}` ); - await this.mempoolService.setStatus( - entries, - MempoolEntryStatus.Submitted, - txHash - ); + await this.setSubmitted(entries, txHash); await this.waitForEntries(entries).catch((err) => this.logger.error(err, "Relayer: Could not find transaction") @@ -159,10 +114,7 @@ export class ClassicRelayer extends BaseRelayer { 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 { @@ -173,7 +125,7 @@ export class ClassicRelayer extends BaseRelayer { this.logger.debug( `User op hashes ${entries.map((entry) => entry.userOpHash)}` ); - await this.mempoolService.removeAll(entries); + await this.setSubmitted(entries, hash); }) .catch((err: any) => this.handleUserOpFail(entries, err)); } diff --git a/packages/executor/src/services/BundlingService/relayers/echo.ts b/packages/executor/src/services/BundlingService/relayers/echo.ts index 2b15456e..4f8c1215 100644 --- a/packages/executor/src/services/BundlingService/relayers/echo.ts +++ b/packages/executor/src/services/BundlingService/relayers/echo.ts @@ -2,7 +2,6 @@ import { providers } from "ethers"; import { PerChainMetrics } from "monitoring/lib"; import { Logger } from "types/lib"; import { IEntryPoint__factory } from "types/lib/executor/contracts"; -import { MempoolEntryStatus } from "types/lib/executor"; import { Config } from "../../../config"; import { Bundle, NetworkConfig } from "../../../interfaces"; import { MempoolService } from "../../MempoolService"; @@ -10,6 +9,7 @@ import { ReputationService } from "../../ReputationService"; import { estimateBundleGasLimit } from "../utils"; import { Relayer } from "../interfaces"; import { now } from "../../../utils"; +import { ExecutorEventBus } from "../../SubscriptionService"; import { BaseRelayer } from "./base"; export class EchoRelayer extends BaseRelayer { @@ -23,6 +23,7 @@ export class EchoRelayer extends BaseRelayer { networkConfig: NetworkConfig, mempoolService: MempoolService, reputationService: ReputationService, + eventBus: ExecutorEventBus, metrics: PerChainMetrics | null ) { super( @@ -33,6 +34,7 @@ export class EchoRelayer extends BaseRelayer { networkConfig, mempoolService, reputationService, + eventBus, metrics ); if (this.networkConfig.echoAuthKey.length === 0) { @@ -77,18 +79,7 @@ export class EchoRelayer extends BaseRelayer { nonce: await relayer.getTransactionCount(), }; - try { - // checking for tx revert - await relayer.estimateGas(transactionRequest); - } catch (err) { - this.logger.debug( - `${entries - .map((entry) => entry.userOpHash) - .join("; ")} failed on chain estimation. deleting...` - ); - this.logger.error(err); - await this.mempoolService.removeAll(entries); - this.reportFailedBundle(); + if (!(await this.validateBundle(relayer, entries, transactionRequest))) { return; } @@ -98,11 +89,7 @@ export class EchoRelayer extends BaseRelayer { this.logger.debug( `Echo: User op hashes ${entries.map((entry) => entry.userOpHash)}` ); - await this.mempoolService.setStatus( - entries, - MempoolEntryStatus.Submitted, - txHash - ); + await this.setSubmitted(entries, txHash); await this.waitForEntries(entries).catch((err) => this.logger.error(err, "Echo: Could not find transaction") ); @@ -112,7 +99,7 @@ export class EchoRelayer extends BaseRelayer { 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("Echo: Timeout"); return; 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..a2f225f1 --- /dev/null +++ b/packages/executor/src/services/BundlingService/relayers/fastlane.ts @@ -0,0 +1,253 @@ +import { providers } from "ethers"; +import { Logger } from "types/lib"; +import { PerChainMetrics } from "monitoring/lib"; +import { IEntryPoint__factory } from "types/lib/executor/contracts"; +import { chainsWithoutEIP1559 } from "params/lib"; +import { AccessList } from "ethers/lib/utils"; +import { 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 { 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, + mempoolService: MempoolService, + reputationService: ReputationService, + eventBus: ExecutorEventBus, + metrics: PerChainMetrics | null + ) { + super( + logger, + chainId, + provider, + config, + networkConfig, + 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 entryPointContract = IEntryPoint__factory.connect( + entryPoint, + this.provider + ); + + const txRequest = entryPointContract.interface.encodeFunctionData( + "handleOps", + [entries.map((entry) => entry.userOp), beneficiary] + ); + + const transactionRequest: providers.TransactionRequest = { + to: entryPoint, + data: txRequest, + type: 2, + maxPriorityFeePerGas: bundle.maxPriorityFeePerGas, + maxFeePerGas: bundle.maxFeePerGas, + }; + + if (this.networkConfig.eip2930) { + const { storageMap } = bundle; + const addresses = Object.keys(storageMap); + if (addresses.length) { + const accessList: AccessList = []; + for (const address of addresses) { + const storageKeys = storageMap[address]; + if (typeof storageKeys == "object") { + accessList.push({ + address, + storageKeys: Object.keys(storageKeys), + }); + } + } + transactionRequest.accessList = accessList; + } + } + + if ( + chainsWithoutEIP1559.some((chainId: number) => chainId === this.chainId) + ) { + transactionRequest.gasPrice = bundle.maxFeePerGas; + delete transactionRequest.maxPriorityFeePerGas; + delete transactionRequest.maxFeePerGas; + delete transactionRequest.type; + delete transactionRequest.accessList; + } + + const transaction = { + ...transactionRequest, + gasLimit: estimateBundleGasLimit( + this.networkConfig.bundleGasLimitMarkup, + bundle.entries + ), + chainId: this.provider._network.chainId, + nonce: await relayer.getTransactionCount(), + }; + + 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 (blockNumber: 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 bd3ef24e..7721e9f2 100644 --- a/packages/executor/src/services/BundlingService/relayers/flashbots.ts +++ b/packages/executor/src/services/BundlingService/relayers/flashbots.ts @@ -6,7 +6,6 @@ import { FlashbotsBundleProvider, FlashbotsBundleResolution, } from "@flashbots/ethers-provider-bundle"; -import { MempoolEntryStatus } from "types/lib/executor"; import { Config } from "../../../config"; import { Bundle, NetworkConfig } from "../../../interfaces"; import { MempoolService } from "../../MempoolService"; @@ -14,6 +13,7 @@ import { ReputationService } from "../../ReputationService"; import { estimateBundleGasLimit } from "../utils"; import { Relayer } from "../interfaces"; import { now } from "../../../utils"; +import { ExecutorEventBus } from "../../SubscriptionService"; import { BaseRelayer } from "./base"; export class FlashbotsRelayer extends BaseRelayer { @@ -27,6 +27,7 @@ export class FlashbotsRelayer extends BaseRelayer { networkConfig: NetworkConfig, mempoolService: MempoolService, reputationService: ReputationService, + eventBus: ExecutorEventBus, metrics: PerChainMetrics | null ) { super( @@ -37,6 +38,7 @@ export class FlashbotsRelayer extends BaseRelayer { networkConfig, mempoolService, reputationService, + eventBus, metrics ); if (!this.networkConfig.rpcEndpointSubmit) { @@ -83,18 +85,7 @@ export class FlashbotsRelayer extends BaseRelayer { nonce: await relayer.getTransactionCount(), }; - try { - // checking for tx revert - await relayer.estimateGas(transactionRequest); - } catch (err) { - this.logger.debug( - `${entries - .map((entry) => entry.userOpHash) - .join("; ")} failed on chain estimation. deleting...` - ); - this.logger.error(err); - await this.mempoolService.removeAll(entries); - this.reportFailedBundle(); + if (!(await this.validateBundle(relayer, entries, transactionRequest))) { return; } @@ -106,22 +97,17 @@ export class FlashbotsRelayer extends BaseRelayer { (entry) => entry.userOpHash )}` ); - await this.mempoolService.setStatus( - entries, - MempoolEntryStatus.Submitted, - txHash - ); + 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 19790911..d3910dee 100644 --- a/packages/executor/src/services/BundlingService/relayers/index.ts +++ b/packages/executor/src/services/BundlingService/relayers/index.ts @@ -3,16 +3,19 @@ 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 EchoRelayer + | typeof FastlaneRelayer; diff --git a/packages/executor/src/services/BundlingService/relayers/kolibri.ts b/packages/executor/src/services/BundlingService/relayers/kolibri.ts index fd2a7a1c..0adb8709 100644 --- a/packages/executor/src/services/BundlingService/relayers/kolibri.ts +++ b/packages/executor/src/services/BundlingService/relayers/kolibri.ts @@ -3,13 +3,13 @@ import { PerChainMetrics } from "monitoring/lib"; import { Logger } from "types/lib"; import { IEntryPoint__factory } from "types/lib/executor/contracts"; import { fetchJson } from "ethers/lib/utils"; -import { MempoolEntryStatus } from "types/lib/executor"; import { Config } from "../../../config"; import { Bundle, NetworkConfig } from "../../../interfaces"; import { MempoolService } from "../../MempoolService"; import { ReputationService } from "../../ReputationService"; import { estimateBundleGasLimit } from "../utils"; import { Relayer } from "../interfaces"; +import { ExecutorEventBus } from "../../SubscriptionService"; import { BaseRelayer } from "./base"; export class KolibriRelayer extends BaseRelayer { @@ -21,6 +21,7 @@ export class KolibriRelayer extends BaseRelayer { networkConfig: NetworkConfig, mempoolService: MempoolService, reputationService: ReputationService, + eventBus: ExecutorEventBus, metrics: PerChainMetrics | null ) { super( @@ -31,6 +32,7 @@ export class KolibriRelayer extends BaseRelayer { networkConfig, mempoolService, reputationService, + eventBus, metrics ); } @@ -72,18 +74,7 @@ export class KolibriRelayer extends BaseRelayer { nonce: await relayer.getTransactionCount(), }; - try { - // checking for tx revert - await relayer.estimateGas(transactionRequest); - } catch (err) { - this.logger.debug( - `${entries - .map((entry) => entry.userOpHash) - .join("; ")} failed on chain estimation. deleting...` - ); - this.logger.error(err); - await this.mempoolService.removeAll(entries); - this.reportFailedBundle(); + if (!(await this.validateBundle(relayer, entries, transactionRequest))) { return; } @@ -94,19 +85,14 @@ export class KolibriRelayer extends BaseRelayer { this.logger.debug( `User op hashes ${entries.map((entry) => entry.userOpHash)}` ); - await this.mempoolService.setStatus( - entries, - MempoolEntryStatus.Submitted, - hash - ); + await this.setSubmitted(entries, hash); await this.waitForEntries(entries).catch((err) => this.logger.error(err, "Kolibri: Could not find transaction") ); - await this.mempoolService.removeAll(entries); }) .catch(async (err) => { this.reportFailedBundle(); - await this.mempoolService.setStatus(entries, MempoolEntryStatus.New); + await this.setNew(entries); await this.handleUserOpFail(entries, err); }); }); diff --git a/packages/executor/src/services/BundlingService/relayers/merkle.ts b/packages/executor/src/services/BundlingService/relayers/merkle.ts index ca1ba2d6..c796b616 100644 --- a/packages/executor/src/services/BundlingService/relayers/merkle.ts +++ b/packages/executor/src/services/BundlingService/relayers/merkle.ts @@ -4,13 +4,13 @@ import { PerChainMetrics } from "monitoring/lib"; import { Logger } from "types/lib"; import { IEntryPoint__factory } from "types/lib/executor/contracts"; import { AccessList, fetchJson } from "ethers/lib/utils"; -import { MempoolEntryStatus } from "types/lib/executor"; import { Config } from "../../../config"; import { Bundle, NetworkConfig } from "../../../interfaces"; import { MempoolService } from "../../MempoolService"; import { ReputationService } from "../../ReputationService"; import { estimateBundleGasLimit } from "../utils"; import { now } from "../../../utils"; +import { ExecutorEventBus } from "../../SubscriptionService"; import { BaseRelayer } from "./base"; export class MerkleRelayer extends BaseRelayer { @@ -24,6 +24,7 @@ export class MerkleRelayer extends BaseRelayer { networkConfig: NetworkConfig, mempoolService: MempoolService, reputationService: ReputationService, + eventBus: ExecutorEventBus, metrics: PerChainMetrics | null ) { super( @@ -34,6 +35,7 @@ export class MerkleRelayer extends BaseRelayer { networkConfig, mempoolService, reputationService, + eventBus, metrics ); if ( @@ -101,18 +103,7 @@ export class MerkleRelayer extends BaseRelayer { } } - try { - // checking for tx revert - await relayer.estimateGas(transactionRequest); - } catch (err) { - this.logger.debug( - `${entries - .map((entry) => entry.userOpHash) - .join("; ")} failed on chain estimation. deleting...` - ); - this.logger.error(err); - await this.mempoolService.removeAll(entries); - this.reportFailedBundle(); + if (!(await this.validateBundle(relayer, entries, transactionRequest))) { return; } @@ -133,16 +124,11 @@ export class MerkleRelayer extends BaseRelayer { this.logger.debug( `User op hashes ${entries.map((entry) => entry.userOpHash)}` ); - await this.mempoolService.setStatus( - entries, - MempoolEntryStatus.Submitted, - hash - ); + await this.setSubmitted(entries, hash); await this.waitForTransaction(hash); - await this.mempoolService.removeAll(entries); } catch (err) { this.reportFailedBundle(); - await this.mempoolService.setStatus(entries, MempoolEntryStatus.New); + await this.setNew(entries); await this.handleUserOpFail(entries, err); } }); diff --git a/packages/executor/src/services/BundlingService/service.ts b/packages/executor/src/services/BundlingService/service.ts index 2229ba3f..8d454ecc 100644 --- a/packages/executor/src/services/BundlingService/service.ts +++ b/packages/executor/src/services/BundlingService/service.ts @@ -23,6 +23,7 @@ import { UserOpValidationService } from "../UserOpValidation"; import { mergeStorageMap } from "../../utils/mergeStorageMap"; import { getAddr, wait } from "../../utils"; import { MempoolEntry } from "../../entities/MempoolEntry"; +import { ExecutorEventBus } from "../SubscriptionService"; import { IRelayingMode } from "./interfaces"; import { ClassicRelayer, @@ -31,6 +32,7 @@ import { RelayerClass, KolibriRelayer, EchoRelayer, + FastlaneRelayer, } from "./relayers"; import { getUserOpGasLimit } from "./utils"; @@ -50,6 +52,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, @@ -61,19 +64,23 @@ export class BundlingService { let Relayer: RelayerClass; if (relayingMode === "flashbots") { - this.logger.debug(`Using flashbots relayer`); + this.logger.debug("Using flashbots relayer"); Relayer = FlashbotsRelayer; } else if (relayingMode === "merkle") { - this.logger.debug(`Using merkle relayer`); + this.logger.debug("Using merkle relayer"); Relayer = MerkleRelayer; } else if (relayingMode === "kolibri") { - this.logger.debug(`Using kolibri relayer`); + this.logger.debug("Using kolibri relayer"); Relayer = KolibriRelayer; } else if (relayingMode === "echo") { - this.logger.debug(`Using echo relayer`); + 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.logger.debug(`Using classic relayer`); + this.logger.debug("Using classic relayer"); Relayer = ClassicRelayer; } this.relayer = new Relayer( @@ -84,6 +91,7 @@ export class BundlingService { this.networkConfig, this.mempoolService, this.reputationService, + this.eventBus, this.metrics ); @@ -189,7 +197,11 @@ export class BundlingService { this.logger.debug( `${title} - ${entity} is banned. Deleting userop ${entry.userOpHash}...` ); - await this.mempoolService.remove(entry); + await this.mempoolService.updateStatus( + entries, + MempoolEntryStatus.Cancelled, + { revertReason: `${title} - ${entity} is banned.` } + ); continue; } else if ( status === ReputationStatus.THROTTLED || @@ -227,7 +239,11 @@ export class BundlingService { this.logger.debug( `${entry.userOpHash} failed 2nd validation: ${e.message}. Deleting...` ); - await this.mempoolService.remove(entry); + await this.mempoolService.updateStatus( + entries, + MempoolEntryStatus.Cancelled, + { revertReason: e.message } + ); continue; } @@ -354,6 +370,10 @@ export class BundlingService { async sendNextBundle(): Promise { await this.mutex.runExclusive(async () => { + if (!(await this.relayer.canSubmitBundle())) { + this.logger.debug("Relayer: Can not submit bundle yet"); + return; + } let relayersCount = this.relayer.getAvailableRelayersCount(); if (relayersCount == 0) { this.logger.debug("Relayers are busy"); @@ -366,18 +386,25 @@ export class BundlingService { this.logger.debug("No new entries"); return; } - // remove entries from mempool if submitAttempts are greater than maxAttemps + // remove entries from mempool if submitAttempts are greater than maxAttempts const invalidEntries = entries.filter( - (entry) => entry.submitAttempts >= this.maxSubmitAttempts + (entry) => entry.submitAttempts > this.maxSubmitAttempts ); if (invalidEntries.length > 0) { this.logger.debug( - `Found ${invalidEntries.length} that reached max submit attemps, deleting them...` + `Found ${invalidEntries.length} that reached max submit attempts, deleting them...` ); this.logger.debug( invalidEntries.map((entry) => entry.userOpHash).join("; ") ); - await this.mempoolService.removeAll(invalidEntries); + await this.mempoolService.updateStatus( + invalidEntries, + MempoolEntryStatus.Cancelled, + { + revertReason: + "Attempted to submit userop multiple times, but failed...", + } + ); entries = await this.mempoolService.getNewEntriesSorted( this.maxBundleSize ); @@ -402,14 +429,23 @@ export class BundlingService { 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.setStatus( + await this.mempoolService.updateStatus( bundle.entries, MempoolEntryStatus.Pending ); await this.mempoolService.attemptToBundle(bundle.entries); - void this.relayer.sendBundle(bundle).catch((err) => { - this.logger.error(err); - }); + + 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 diff --git a/packages/executor/src/services/EventsService.ts b/packages/executor/src/services/EventsService.ts index b0062ef9..8b25c2b2 100644 --- a/packages/executor/src/services/EventsService.ts +++ b/packages/executor/src/services/EventsService.ts @@ -7,10 +7,15 @@ import { SignatureAggregatorChangedEvent, UserOperationEventEvent, } from "types/lib/executor/contracts/EntryPoint"; -import { TypedEvent } from "types/lib/executor/contracts/common"; +import { TypedEvent, TypedListener } from "types/lib/executor/contracts/common"; +import { MempoolEntryStatus } from "types/lib/executor"; import { ReputationService } from "./ReputationService"; import { MempoolService } from "./MempoolService"; +import { ExecutorEvent, ExecutorEventBus } from "./SubscriptionService"; +/** + * Listens for events in the blockchain + */ export class EventsService { private entryPoints: IEntryPoint[] = []; private lastBlockPerEntryPoint: { @@ -24,6 +29,7 @@ export class EventsService { private logger: Logger, private reputationService: ReputationService, private mempoolService: MempoolService, + private eventBus: ExecutorEventBus, private entryPointAddrs: string[], private db: IDbController ) { @@ -38,11 +44,25 @@ export class EventsService { for (const contract of this.entryPoints) { contract.on(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 { + for (const contract of this.entryPoints) { + contract.on(contract.filters.UserOperationEvent(), callback); + } + } + + offUserOperationEvent( + callback: TypedListener + ): void { + for (const contract of this.entryPoints) { + contract.off(contract.filters.UserOperationEvent(), callback); + } + } + /** * manually handle all new events since last run */ @@ -71,12 +91,7 @@ export class EventsService { 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); @@ -123,7 +138,12 @@ export class EventsService { this.logger.debug( `Found UserOperationEvent for ${ev.args.userOpHash}. Deleting userop...` ); - await this.mempoolService.remove(entry); + 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); @@ -150,3 +170,8 @@ export class EventsService { } } } + +type ParsedEventType = + | UserOperationEventEvent + | AccountDeployedEvent + | SignatureAggregatorChangedEvent; diff --git a/packages/executor/src/services/MempoolService.ts b/packages/executor/src/services/MempoolService.ts deleted file mode 100644 index 1e61ceac..00000000 --- a/packages/executor/src/services/MempoolService.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { BigNumberish, utils } from "ethers"; -import { IDbController, Logger } from "types/lib"; -import RpcError from "types/lib/api/errors/rpc-error"; -import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; -import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint"; -import { - IEntityWithAggregator, - MempoolEntryStatus, - IWhitelistedEntities, - ReputationStatus, -} from "types/lib/executor"; -import { Mutex } from "async-mutex"; -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"; - -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 - private mutex = new Mutex(); - - constructor( - private db: IDbController, - private chainId: number, - private reputationService: ReputationService, - private networkConfig: NetworkConfig, - private logger: Logger - ) { - 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: UserOperationStruct, - 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: getAddr(userOp.initCode), - paymaster: getAddr(userOp.paymasterAndData), - 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.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); - this.logger.debug("Mempool: User op added"); - } - await this.updateSeenStatus(userOp, aggregator); - }); - } - - async removeAll(entries: MempoolEntry[]): Promise { - for (const entry of entries) { - await this.remove(entry); - } - } - - async remove(entry: MempoolEntry | null): Promise { - if (!entry) { - return; - } - 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`); - }); - } - - async attemptToBundle(entries: MempoolEntry[]): Promise { - await this.mutex.runExclusive(async () => { - 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 { - await this.mutex.runExclusive(async () => { - 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 { - 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 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: UserOperationStruct, - 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 } = 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 - ); - } - - if (userOp.paymasterAndData.length >= 42) { - const paymaster = utils.getAddress(getAddr(userOp.paymasterAndData)!); - if (accounts.includes(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 - ); - } - } - - if (userOp.initCode.length >= 42) { - const factory = utils.getAddress(getAddr(userOp.initCode)!); - if (accounts.includes(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, - submittedTime: raw.submittedTime, - }); - } - - /** - * 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( - userOp: UserOperationStruct, - aggregator?: string - ): Promise { - const paymaster = getAddr(userOp.paymasterAndData); - const factory = getAddr(userOp.initCode); - 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..fff48687 --- /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 minutse 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..66a21d8c --- /dev/null +++ b/packages/executor/src/services/MempoolService/reputation.ts @@ -0,0 +1,185 @@ +import { utils } from "ethers"; +import RpcError from "types/lib/api/errors/rpc-error"; +import { + IEntityWithAggregator, + IWhitelistedEntities, + ReputationStatus, +} from "types/lib/executor"; +import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; +import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint"; +import { MempoolEntry } from "../../entities/MempoolEntry"; +import { KnownEntities, NetworkConfig, StakeInfo } from "../../interfaces"; +import { ReputationService } from "../ReputationService"; +import { getAddr } from "../../utils"; +import { MempoolService } from "./service"; +import { + MAX_MEMPOOL_USEROPS_PER_SENDER, + THROTTLED_ENTITY_MEMPOOL_COUNT, +} from "./constants"; + +export class MempoolReputationChecks { + constructor( + private service: MempoolService, + 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 } = 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 + ); + } + + if (userOp.paymasterAndData.length >= 42) { + const paymaster = utils.getAddress(getAddr(userOp.paymasterAndData)!); + if (accounts.includes(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 + ); + } + } + + if (userOp.initCode.length >= 42) { + const factory = utils.getAddress(getAddr(userOp.initCode)!); + if (accounts.includes(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( + userOp: UserOperationStruct, + aggregator?: string + ): Promise { + const paymaster = getAddr(userOp.paymasterAndData); + const factory = getAddr(userOp.initCode); + 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..5381705b --- /dev/null +++ b/packages/executor/src/services/MempoolService/service.ts @@ -0,0 +1,303 @@ +import { Mutex } from "async-mutex"; +import { IDbController, Logger } from "types/lib"; +import { MempoolEntryStatus } from "types/lib/executor"; +import RpcError from "types/lib/api/errors/rpc-error"; +import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes"; +import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint"; +import { BigNumberish } from "ethers"; +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 { getAddr, now } from "../../utils"; +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 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.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: UserOperationStruct, + 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: UserOperationStruct, + 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: getAddr(userOp.initCode), + paymaster: getAddr(userOp.paymasterAndData), + 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(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..5d8c387c --- /dev/null +++ b/packages/executor/src/services/MempoolService/utils.ts @@ -0,0 +1,8 @@ +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 e406370b..aef4ebc4 100644 --- a/packages/executor/src/services/P2PService.ts +++ b/packages/executor/src/services/P2PService.ts @@ -15,17 +15,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 + 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/SubscriptionService.ts b/packages/executor/src/services/SubscriptionService.ts new file mode 100644 index 00000000..b79805df --- /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 "types/lib"; +import { deepHexlify } from "utils/lib/hexlify"; +import { MempoolEntryStatus } from "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/index.ts b/packages/executor/src/services/index.ts index 0dc88f4e..8605a68f 100644 --- a/packages/executor/src/services/index.ts +++ b/packages/executor/src/services/index.ts @@ -4,3 +4,4 @@ export * from "./BundlingService"; export * from "./ReputationService"; export * from "./EventsService"; export * from "./P2PService"; +export * from "./SubscriptionService"; diff --git a/packages/executor/src/utils/index.ts b/packages/executor/src/utils/index.ts index 44e7a678..4073d6bc 100644 --- a/packages/executor/src/utils/index.ts +++ b/packages/executor/src/utils/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { BytesLike, defaultAbiCoder, @@ -109,32 +110,6 @@ export function getUserOpHash( return keccak256(enc); } -/** - * 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) { return undefined; diff --git a/packages/executor/test/fixtures/getConfig.ts b/packages/executor/test/fixtures/getConfig.ts index b7a7a868..28fb773e 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 = { @@ -28,7 +32,7 @@ const BaseConfig: ConfigOptions = { useropsTTL: 300, whitelistedEntities: { paymaster: [], account: [], factory: [] }, bundleGasLimitMarkup: 25000, - bundleInterval: 10000, + bundleInterval: 100, bundleSize: 4, relayingMode: "classic", pvgMarkup: 0, @@ -39,30 +43,31 @@ const BaseConfig: ConfigOptions = { skipBundleValidation: false, entryPointForwarder: "", gasFeeInSimulation: true, - userOpGasLimit: 1000000, - bundleGasLimit: 6000000, + userOpGasLimit: 10000000, + bundleGasLimit: 60000000, merkleApiURL: "", echoAuthKey: "", - kolibriAuthKey: "" + kolibriAuthKey: "", + archiveDuration: 60, // 1 minute }, 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(); } @@ -71,5 +76,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 8e9bbf9e..a37103cf 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 { getServices } from "./services"; import { Web3, Debug, Eth } from "../../src/modules"; +import { getServices } from "./services"; export async function getModules(config: Config, networkConfig: NetworkConfig) { const provider = config.getNetworkProvider()!; @@ -12,12 +12,13 @@ export async function getModules(config: Config, networkConfig: NetworkConfig) { userOpValidationService, mempoolService, bundlingService, - skandha + skandha, + eventsService, } = await getServices(config, networkConfig); const web3 = new Web3(config, { version: "test", - commit: "commit" + commit: "commit", }); const debug = new Debug( provider, @@ -48,5 +49,6 @@ export async function getModules(config: Config, networkConfig: NetworkConfig) { userOpValidationService, mempoolService, bundlingService, - } + eventsService, + }; } diff --git a/packages/executor/test/fixtures/services.ts b/packages/executor/test/fixtures/services.ts index e621e8c6..62407992 100644 --- a/packages/executor/test/fixtures/services.ts +++ b/packages/executor/test/fixtures/services.ts @@ -1,13 +1,24 @@ import { BigNumber } from "ethers"; import { Config } from "../../src/config"; import { NetworkConfig } from "../../src/interfaces"; -import { BundlingService, EventsService, MempoolService, ReputationService, UserOpValidationService } from "../../src/services"; +import { + BundlingService, + EventsService, + MempoolService, + ReputationService, + UserOpValidationService, + SubscriptionService, + ExecutorEventBus +} 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( @@ -20,13 +31,10 @@ export async function getServices(config: Config, networkConfig: NetworkConfig) networkConfig.minUnstakeDelay ); - const skandha = new Skandha( - undefined, - ChainId, - provider, - config, - logger - ); + const skandha = new Skandha(undefined, ChainId, provider, config, logger); + + const eventBus = new ExecutorEventBus(); + const subscriptionService = new SubscriptionService(eventBus, logger); const userOpValidationService = new UserOpValidationService( skandha, @@ -41,6 +49,7 @@ export async function getServices(config: Config, networkConfig: NetworkConfig) db, ChainId, reputationService, + eventBus, networkConfig, logger ); @@ -65,9 +74,11 @@ export async function getServices(config: Config, networkConfig: NetworkConfig) logger, reputationService, mempoolService, + eventBus, networkConfig.entryPoints, db ); + eventsService.initEventListener(); return { reputationService, @@ -75,6 +86,8 @@ export async function getServices(config: Config, networkConfig: NetworkConfig) mempoolService, bundlingService, eventsService, - skandha - } -} \ No newline at end of file + skandha, + subscriptionService, + eventBus, + }; +} diff --git a/packages/executor/test/unit/services/BundlingService.test.ts b/packages/executor/test/unit/services/BundlingService.test.ts index fa5d227b..e872916c 100644 --- a/packages/executor/test/unit/services/BundlingService.test.ts +++ b/packages/executor/test/unit/services/BundlingService.test.ts @@ -1,14 +1,30 @@ -import { describe, it, expect } from "vitest"; -import { getConfigs, getModules, getClient, getWallet, createSignedUserOp, getCounterFactualAddress } from "../../fixtures"; +import { describe, it, expect, vi } from "vitest"; +import { MempoolEntryStatus } from "types/src/executor"; +import { UserOperationEventEvent } from "types/src/executor/contracts/EntryPoint"; +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 { + ethModule, + networkConfigUnsafe, + debugModule, + mempoolService, + eventsService, + } = await prepareTest(); describe("Unsafe mode", async () => { - it("Submitted bundle should contain configured number of userops", async () => { + it.skip("Submitted bundle should contain configured number of userops", async () => { + expect(networkConfigUnsafe.bundleInterval).toBeLessThan(300); const { bundleSize } = networkConfigUnsafe; const userOpHashes = []; for (let i = 0; i < bundleSize; ++i) { @@ -18,34 +34,103 @@ 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]); - if (!txHash) { - txHash = response?.transactionHash; - } else { - expect(response?.transactionHash).toEqual(txHash); - } + const success = await new Promise((resolve) => { + let trx: string | null = null; + let foundEvents = 0; + const callback = (...args: any[]): void => { + const event = args[args.length - 1] as UserOperationEventEvent; + if (trx == null) { + trx = event.transactionHash; + } + if (trx != event.transactionHash) { + eventsService.offUserOperationEvent(callback); + resolve(false); + } + if (++foundEvents == bundleSize) { + eventsService.offUserOperationEvent(callback); + resolve(trx != null); + } + }; + eventsService.onUserOperationEvent(callback); + }); + + expect(success).toBeTruthy(); + }); + + it("updates entry status after submitting userop", async () => { + const wallet = await getWallet(); + const aaWallet = await getCounterFactualAddress(wallet.address); + await setBalance(aaWallet); + const userOp = await createSignedUserOp(ethModule, wallet); + const hash = await ethModule.sendUserOperation({ + userOp, + entryPoint: EntryPointAddress, + }); + let entry = await mempoolService.getEntryByHash(hash); + if (!entry) { + return expect.unreachable("Could not find userop"); } + expect( + entry.status === MempoolEntryStatus.New, + "Invalid status. Must be New" + ).toBeTruthy(); + + await debugModule.sendBundleNow(); + + const success = await new Promise((resolve) => { + const callback = async (...args: any[]): Promise => { + eventsService.offUserOperationEvent(callback); + entry = await mempoolService.getEntryByHash(hash); + if (!entry) { + return resolve(false); + } + expect(entry.status === MempoolEntryStatus.OnChain).toBeTruthy(); + expect(entry.actualTransaction).toEqual(entry.transaction); + resolve(true); + }; + eventsService.onUserOperationEvent(callback); + }); + + if (!success) { + expect.unreachable("Could not find userop"); + } + + await mempoolService.deleteOldUserOps(); + expect(await mempoolService.getEntryByHash(hash)).not.toBeNull(); + vi.useFakeTimers(); + vi.advanceTimersByTime(2 * networkConfigUnsafe.archiveDuration * 1000); + + await mempoolService.deleteOldUserOps(); + expect(await mempoolService.getEntryByHash(hash)).toBeNull(); + vi.useRealTimers(); }); }); }); +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type async function prepareTest() { const { configUnsafe, networkConfigUnsafe } = await getConfigs(); const { eth: ethModule, bundlingService: service, - debug: debugModule + debug: debugModule, + mempoolService, + eventsService, } = await getModules(configUnsafe, networkConfigUnsafe); - return { service, ethModule, networkConfigUnsafe, debugModule }; -}; + return { + service, + ethModule, + networkConfigUnsafe, + debugModule, + mempoolService, + eventsService, + }; +} diff --git a/packages/monitoring/package.json b/packages/monitoring/package.json index 81a93a3e..c7de74c5 100644 --- a/packages/monitoring/package.json +++ b/packages/monitoring/package.json @@ -1,6 +1,6 @@ { "name": "monitoring", - "version": "1.5.4", + "version": "1.5.5", "description": "The Monitoring module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://github.com/etherspot/etherspot-bundler#readme", @@ -29,6 +29,6 @@ }, "dependencies": { "prom-client": "^14.2.0", - "types": "^1.5.4" + "types": "^1.5.5" } } diff --git a/packages/node/package.json b/packages/node/package.json index 88e35e2a..fb4875d9 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "node", - "version": "1.5.4", + "version": "1.5.5", "description": "The bundler node module of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -54,24 +54,24 @@ "@multiformats/multiaddr": "11.4.0", "@types/varint": "6.0.1", "abstract-leveldown": "7.2.0", - "api": "^1.5.4", + "api": "^1.5.5", "datastore-core": "8.0.1", - "db": "^1.5.4", + "db": "^1.5.5", "ethers": "5.7.2", - "executor": "^1.5.4", + "executor": "^1.5.5", "it-filter": "1.0.2", "it-map": "1.0.5", "it-sort": "1.0.0", "it-take": "1.0.1", "libp2p": "0.42.2", - "monitoring": "^1.5.4", - "params": "^1.5.4", + "monitoring": "^1.5.5", + "params": "^1.5.5", "snappy": "7.2.2", "snappyjs": "0.7.0", "stream-to-it": "0.2.4", "strict-event-emitter-types": "2.0.0", - "types": "^1.5.4", - "utils": "^1.5.4", + "types": "^1.5.5", + "utils": "^1.5.5", "varint": "6.0.0", "xxhash-wasm": "1.0.2" }, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index dae2d52f..2ee4aa50 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: server, config: relayersConfig, testingMode, redirectRpc, diff --git a/packages/node/src/network/peers/discover.ts b/packages/node/src/network/peers/discover.ts index 47bd2a55..792da117 100644 --- a/packages/node/src/network/peers/discover.ts +++ b/packages/node/src/network/peers/discover.ts @@ -225,7 +225,7 @@ export class PeerDiscovery { } // async due to some crypto that's no longer necessary const peerId = await enr.peerId(); - // tcp multiaddr is known to be be present, checked inside the worker + // tcp multiaddr is known to be present, checked inside the worker const multiaddrTCP = enr.getLocationMultiaddr(ENRKey.tcp); if (!multiaddrTCP) { this.logger.error("Discv5 worker sent enr without tcp multiaddr", { diff --git a/packages/node/src/network/peers/peerManager.ts b/packages/node/src/network/peers/peerManager.ts index f1a8cc06..c4429b05 100644 --- a/packages/node/src/network/peers/peerManager.ts +++ b/packages/node/src/network/peers/peerManager.ts @@ -14,6 +14,7 @@ import { getConnection, prettyPrintPeerId } from "../../utils/network"; import { BundlerGossipsub } from "../gossip/handler"; import { ReqRespMethod, RequestTypedContainer } from "../reqresp"; import { IReqRespNode } from "../reqresp/interface"; +import { StatusCache } from "../statusCache"; import { PeersData, PeerData } from "./peersData"; import { PeerDiscovery } from "./discover"; import { IPeerRpcScoreStore, ScoreState, updateGossipsubScores } from "./score"; @@ -23,7 +24,6 @@ import { hasSomeConnectedPeer, prioritizePeers, } from "./utils"; -import { StatusCache } from "../statusCache"; /** heartbeat performs regular updates such as updating reputations and performing discovery requests */ const HEARTBEAT_INTERVAL_MS = 15 * 1000; @@ -46,7 +46,7 @@ const ALLOWED_NEGATIVE_GOSSIPSUB_FACTOR = 0.1; // TODO: // maxPeers and targetPeers should be dynamic on the num of validators connected -// The Node should compute a recomended value every interval and log a warning +// The Node should compute a recommended value every interval and log a warning // to terminal if it deviates significantly from the user's settings export type PeerManagerOpts = { @@ -88,7 +88,7 @@ enum RelevantPeerStatus { } /** - * Performs all peer managment functionality in a single grouped class: + * Performs all peer management functionality in a single grouped class: * - Ping peers every `PING_INTERVAL_MS` * - Status peers every `STATUS_INTERVAL_MS` * - Execute discovery query if under target peers @@ -131,7 +131,7 @@ export class PeerManager { discv5FirstQueryDelayMs: opts.discv5FirstQueryDelayMs, discv5: opts.discv5, connectToDiscv5Bootnodes: opts.connectToDiscv5Bootnodes, - chainId: opts.chainId + chainId: opts.chainId, })); } @@ -339,12 +339,12 @@ export class PeerManager { } } - private async requestStatus(peer: PeerId, localStatus: ts.Status): Promise { + private async requestStatus( + peer: PeerId, + localStatus: ts.Status + ): Promise { try { - this.onStatus( - peer, - await this.reqResp.status(peer, localStatus) - ); + this.onStatus(peer, await this.reqResp.status(peer, localStatus)); } catch (e) { // TODO: Failed to get peer latest status: downvote but don't disconnect } @@ -388,7 +388,10 @@ export class PeerManager { this.opts ); - this.logger.debug(connectedHealthyPeers, `peersToConnect: ${peersToConnect}`); + this.logger.debug( + connectedHealthyPeers, + `peersToConnect: ${peersToConnect}` + ); // disconnect first to have more slots before we dial new peers // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/node/src/network/reqresp/ReqRespNode.ts b/packages/node/src/network/reqresp/ReqRespNode.ts index 0c7758ab..275bb5fe 100644 --- a/packages/node/src/network/reqresp/ReqRespNode.ts +++ b/packages/node/src/network/reqresp/ReqRespNode.ts @@ -206,7 +206,7 @@ export class ReqRespNode extends ReqResp implements IReqRespNode { versions: number[], body: Req ): AsyncIterable { - // Remember prefered encoding + // Remember preferred encoding const encoding = this.peersData.getEncodingPreference(peerId.toString()) ?? Encoding.SSZ_SNAPPY; diff --git a/packages/node/src/reqresp/request/errors.ts b/packages/node/src/reqresp/request/errors.ts index da51b1a7..896d9c67 100644 --- a/packages/node/src/reqresp/request/errors.ts +++ b/packages/node/src/reqresp/request/errors.ts @@ -21,7 +21,7 @@ export enum RequestErrorCode { REQUEST_TIMEOUT = "REQUEST_ERROR_REQUEST_TIMEOUT", /** Error when sending request to responder */ REQUEST_ERROR = "REQUEST_ERROR_REQUEST_ERROR", - /** Reponder did not deliver a full reponse before max maxTotalResponseTimeout() */ + /** Reponder did not deliver a full response before max maxTotalResponseTimeout() */ RESPONSE_TIMEOUT = "REQUEST_ERROR_RESPONSE_TIMEOUT", /** A single-response method returned 0 chunks */ EMPTY_RESPONSE = "REQUEST_ERROR_EMPTY_RESPONSE", diff --git a/packages/node/src/reqresp/request/index.ts b/packages/node/src/reqresp/request/index.ts index e5376356..ece15408 100644 --- a/packages/node/src/reqresp/request/index.ts +++ b/packages/node/src/reqresp/request/index.ts @@ -47,7 +47,7 @@ type SendRequestModules = { * * 1. Dial peer, establish duplex stream * 2. Encoded and write request to peer. Expect the responder to close the stream's write side - * 3. Read and decode reponse(s) from peer. Will close the read stream if: + * 3. Read and decode response(s) from peer. Will close the read stream if: * - An error result is received in one of the chunks. Reads the error_message and throws. * - The responder closes the stream. If at the end or start of a , return. Otherwise throws * - Any part of the response_chunk fails validation. Throws a typed error (see `SszSnappyError`) diff --git a/packages/params/package.json b/packages/params/package.json index 85e31db4..b38f614b 100644 --- a/packages/params/package.json +++ b/packages/params/package.json @@ -1,6 +1,6 @@ { "name": "params", - "version": "1.5.4", + "version": "1.5.5", "description": "Various bundler parameters", "author": "Etherspot", "homepage": "https://github.com/etherspot/skandha#readme", @@ -26,8 +26,8 @@ "@eth-optimism/sdk": "3.2.3", "@mantleio/sdk": "0.2.1", "ethers": "5.7.2", - "types": "^1.5.4", - "utils": "^1.5.4" + "types": "^1.5.5", + "utils": "^1.5.5" }, "scripts": { "clean": "rm -rf lib && rm -f *.tsbuildinfo", diff --git a/packages/params/src/gas-price-oracles/oracles/ancient8.ts b/packages/params/src/gas-price-oracles/oracles/ancient8.ts new file mode 100644 index 00000000..05f5b650 --- /dev/null +++ b/packages/params/src/gas-price-oracles/oracles/ancient8.ts @@ -0,0 +1,55 @@ +import { fetchJson, hexValue } from "ethers/lib/utils"; +import { BigNumber } from "ethers"; +import { parseGwei } from "./utils"; +import { IGetGasFeeResult, IOracle } from "./interfaces"; + +export const getAncient8GasFee: IOracle = + async (): Promise => { + const { gas_prices }: Ancient8Response = await fetchJson({ + url: "https://scan.ancient8.gg/api/v2/stats", + headers: { + "updated-gas-oracle": "true", + }, + }); + const maxPriorityFeePerGas = hexValue( + BigNumber.from(gas_prices.fast.priority_fee_wei) + ); + const maxFeePerGas = parseGwei(gas_prices.fast.priority_fee); + return { + maxPriorityFeePerGas: maxPriorityFeePerGas, + gasPrice: maxFeePerGas, + maxFeePerGas: maxFeePerGas, + }; + }; + +type Ancient8Response = { + gas_prices: { + average: { + base_fee: number; + fiat_price: string; + price: number; + priority_fee: number; + priority_fee_wei: string; + time: number; + wei: string; + }; + fast: { + base_fee: number; + fiat_price: string; + price: number; + priority_fee: number; + priority_fee_wei: string; + time: number; + wei: string; + }; + slow: { + base_fee: number; + fiat_price: string; + price: number; + priority_fee: number; + priority_fee_wei: string; + time: number; + wei: string; + }; + }; +}; diff --git a/packages/params/src/gas-price-oracles/oracles/index.ts b/packages/params/src/gas-price-oracles/oracles/index.ts index 17641f91..46a0b0f9 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 { getBaseGasFee } from "./base"; +import { getAncient8GasFee } from "./ancient8"; export const oracles: { [chainId: number]: IOracle | undefined; @@ -19,4 +20,5 @@ export const oracles: { 5000: getMantleGasFee, 5001: getMantleGasFee, 8453: getBaseGasFee, + 888888888: getAncient8GasFee, }; diff --git a/packages/types/package.json b/packages/types/package.json index dc6c97d0..0d7af410 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "types", - "version": "1.5.4", + "version": "1.5.5", "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 4ef3752d..f8926d0e 100644 --- a/packages/types/src/api/interfaces.ts +++ b/packages/types/src/api/interfaces.ts @@ -32,6 +32,14 @@ export type GetFeeHistoryResponse = { maxPriorityFeePerGas: BigNumberish[]; }; +export type UserOperationStatus = { + userOp: UserOperationStruct; + entryPoint: string; + status: string; + transaction?: string; + reason?: string; +}; + export type UserOperationReceipt = { userOpHash: string; sender: string; @@ -86,6 +94,8 @@ export type GetConfigResponse = { gasFeeInSimulation: boolean; userOpGasLimit: number; bundleGasLimit: number; + archiveDuration: number; + fastlaneValidators: string[]; }; export type SupportedEntryPoints = string[]; @@ -99,4 +109,6 @@ export interface ServerConfig { port: number; host: string; cors: string; + ws: boolean; + wsPort: number; } 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 8a1fabf1..6812901e 100644 --- a/packages/types/src/executor/index.ts +++ b/packages/types/src/executor/index.ts @@ -9,7 +9,8 @@ export type RelayingMode = | "flashbots" | "classic" | "kolibri" - | "echo"; + | "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 8f70feee..0248ee32 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "utils", - "version": "1.5.4", + "version": "1.5.5", "description": "utils of Etherspot bundler client", "author": "Etherspot", "homepage": "https://https://github.com/etherspot/skandha#readme", @@ -32,8 +32,9 @@ "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", - "types": "^1.5.4" + "types": "^1.5.5" } } 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/tsconfig.build.json b/tsconfig.build.json index d0e38040..e4bdf53c 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -24,6 +24,6 @@ "declarationMap": true, "incremental": true, "preserveWatchOutput": true, - "experimentalDecorators": true - } + "experimentalDecorators": true, + }, } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index dadd2958..8d018172 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,6 @@ "incremental": false, // Required to run benchmark command from root directory "typeRoots": ["node_modules/@types", "./types"], - "noEmit": true - } + "noEmit": true, + }, } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8b3a1525..3b2c9b49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -396,6 +396,18 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== +"@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.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + "@eslint/eslintrc@^1.3.3": version "1.4.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" @@ -859,20 +871,20 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== -"@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/error@^3.0.0", "@fastify/error@^3.3.0": +"@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== @@ -886,6 +898,15 @@ 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" @@ -3314,13 +3335,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@*", "@types/dns-packet@^5.6.5": version "5.6.5" resolved "https://registry.yarnpkg.com/@types/dns-packet/-/dns-packet-5.6.5.tgz#49fc29a40f5d30227ed028fa1ee82601d3745e15" @@ -3373,7 +3387,7 @@ resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== -"@types/json-schema@^7.0.9": +"@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== @@ -3461,7 +3475,7 @@ "@types/abstract-leveldown" "*" "@types/node" "*" -"@types/semver@^7.3.12": +"@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== @@ -3522,6 +3536,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.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -3534,88 +3555,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" + 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@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== +"@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" "5.43.0" - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/typescript-estree" "5.43.0" + "@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" + 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@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== +"@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" "5.43.0" - eslint-visitor-keys "^3.3.0" + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" "@vitest/coverage-v8@0.34.6": version "0.34.6" @@ -4038,7 +4062,7 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -avvio@^8.2.0: +avvio@^8.2.1: version "8.3.0" resolved "https://registry.yarnpkg.com/avvio/-/avvio-8.3.0.tgz#1e019433d935730b814978a583eefac41a65082f" integrity sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q== @@ -5088,6 +5112,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" + ejs@^3.1.7: version "3.1.9" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" @@ -5361,14 +5395,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" @@ -5472,11 +5498,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" @@ -5624,7 +5645,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== @@ -5681,7 +5702,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-json-stringify@^5.7.0: +fast-json-stringify@^5.7.0, fast-json-stringify@^5.8.0: version "5.14.1" resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-5.14.1.tgz#3b1aa5a823e4dd5414ec079d32f51e33dd887766" integrity sha512-J1Grbf0oSXV3lKsBf3itz1AvRk43qVrx3Ac10sNvi3LZaz1by4oDdYKFrJycPhS8+Gb7y8rgV/Jqw1UZVjyNvw== @@ -5726,26 +5747,27 @@ 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, fastq@^1.6.0: version "1.17.1" @@ -5787,7 +5809,7 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-my-way@^7.3.0: +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== @@ -6213,6 +6235,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" @@ -6432,7 +6459,7 @@ ignore-walk@^5.0.1: dependencies: minimatch "^5.0.1" -ignore@^5.0.4, ignore@^5.2.0: +ignore@^5.0.4, ignore@^5.2.0, 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== @@ -7453,7 +7480,7 @@ 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: +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== @@ -7795,6 +7822,13 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@9.0.3: + 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" @@ -7914,10 +7948,10 @@ mlly@^1.2.0, mlly@^1.4.0: pkg-types "^1.0.3" ufo "^1.3.2" -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" @@ -8051,11 +8085,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" @@ -8869,7 +8898,7 @@ pino@8.11.0: sonic-boom "^3.1.0" thread-stream "^2.0.0" -pino@^8.5.0: +pino@^8.17.0: version "8.20.0" resolved "https://registry.yarnpkg.com/pino/-/pino-8.20.0.tgz#ccfc6fef37b165e006b923834131632a8c4f036b" integrity sha512-uhIfMj5TVp+WynVASaVEJFTncTUe4dHBq6CWplu/vBgvGHhvBvQfxz+vcOrnnBQdORH3izaGEurLfNlq3YxdFQ== @@ -9536,7 +9565,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== @@ -9858,6 +9887,11 @@ std-env@^3.3.3: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== +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" @@ -10102,11 +10136,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" @@ -10148,6 +10177,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" @@ -10175,6 +10209,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" @@ -10222,23 +10261,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" - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -10337,10 +10364,10 @@ 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@5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== "typescript@^3 || ^4": version "4.9.5" @@ -10841,6 +10868,11 @@ 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, ws@^8.0.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + xml2js@^0.6.0, xml2js@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499"