diff --git a/.eslintrc.js b/.eslintrc.js index 7244b9d..9d38530 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,36 +1,91 @@ module.exports = { root: true, - extends: ["@metamask/eslint-config"], + extends: ['@metamask/eslint-config'], overrides: [ { - files: ["*.ts"], - extends: ["@metamask/eslint-config-typescript"], + files: ['*.ts'], + extends: ['@metamask/eslint-config-typescript'], + parserOptions: { + project: ['./tsconfig.json', './tsconfig.packages.json'], + }, + rules: { + camelcase: 'off', + 'consistent-return': 'off', + 'default-case': 'off', + 'guard-for-in': 'off', + 'id-denylist': 'off', + 'id-length': 'off', + 'import/no-mutable-exports': 'off', + 'import/no-nodejs-modules': 'off', + 'import/no-unassigned-import': 'off', + 'import/order': 'off', + 'jsdoc/check-indentation': 'off', + 'jsdoc/check-param-names': 'off', + 'jsdoc/match-description': 'off', + 'jsdoc/require-description': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns': 'off', + 'no-constant-condition': 'off', + 'no-eq-null': 'off', + 'no-lonely-if': 'off', + 'no-multi-assign': 'off', + 'no-negated-condition': 'off', + 'no-param-reassign': 'off', + 'no-plusplus': 'off', + 'no-restricted-globals': 'off', + 'no-restricted-syntax': 'off', + 'no-void': 'off', + 'prefer-const': 'off', + 'prefer-rest-params': 'off', + 'pretter/prettier': 'off', + radix: 'off', + 'require-unicode-regexp': 'off', + '@typescript-eslint/await-thenable': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/consistent-type-imports': 'off', + '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-parameter-properties': 'off', + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-shadow': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/prefer-enum-initializers': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/switch-exhaustiveness-check': 'off', + '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/unified-signatures': 'off', + }, }, { - files: ["*.js"], + files: ['*.js'], parserOptions: { - sourceType: "script", + sourceType: 'script', }, - extends: ["@metamask/eslint-config-nodejs"], + extends: ['@metamask/eslint-config-nodejs'], }, { - files: ["*.test.ts", "*.test.js"], - extends: [ - "@metamask/eslint-config-jest", - "@metamask/eslint-config-nodejs", - ], + files: ['*.test.ts', '*.test.js'], + extends: ['@metamask/eslint-config-nodejs'], }, ], ignorePatterns: [ - "!.eslintrc.js", - "!.prettierrc.js", - "dist/", - "docs/", - ".yarn/", + '!.eslintrc.js', + '!.prettierrc.js', + 'dist/', + 'docs/', + '.yarn/', + 'tsconfig.json', ], + + rules: {}, }; diff --git a/.prettierrc.js b/.prettierrc.js index 0195124..b2d98d2 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,9 +1,8 @@ // All of these are defaults except singleQuote, but we specify them // for explicitness module.exports = { - quoteProps: 'as-needed', - singleQuote: true, - tabWidth: 2, - trailingComma: 'all', - }; - \ No newline at end of file + quoteProps: 'as-needed', + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', +}; diff --git a/hardhat.config.ts b/hardhat.config.ts index 21a7575..2a3706d 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,21 +1,24 @@ -import "@nomiclabs/hardhat-ethers"; -import "@nomicfoundation/hardhat-toolbox"; -import "hardhat-deploy"; +import '@nomiclabs/hardhat-ethers'; +import '@nomicfoundation/hardhat-toolbox'; +import 'hardhat-deploy'; -import fs from "fs"; - -import { HardhatUserConfig } from "hardhat/config"; -import { NetworkUserConfig } from "hardhat/src/types/config"; +import fs from 'fs'; +import type { HardhatUserConfig } from 'hardhat/config'; +import type { NetworkUserConfig } from 'hardhat/src/types/config'; const mnemonicFileName = process.env.MNEMONIC_FILE; -let mnemonic = "test ".repeat(11) + "junk"; +let mnemonic = `${'test '.repeat(11)}junk`; if (mnemonicFileName != null && fs.existsSync(mnemonicFileName)) { - mnemonic = fs.readFileSync(mnemonicFileName, "ascii").trim(); + mnemonic = fs.readFileSync(mnemonicFileName, 'ascii').trim(); } const infuraUrl = (name: string): string => `https://${name}.infura.io/v3/${process.env.INFURA_ID}`; +/** + * + * @param url + */ function getNetwork(url: string): NetworkUserConfig { return { url, @@ -25,27 +28,31 @@ function getNetwork(url: string): NetworkUserConfig { }; } +/** + * + * @param name + */ function getInfuraNetwork(name: string): NetworkUserConfig { return getNetwork(infuraUrl(name)); } const config: HardhatUserConfig = { paths: { - sources: "src/contracts", + sources: 'src/contracts', }, typechain: { - outDir: "src/contract-types", - target: "ethers-v5", + outDir: 'src/contract-types', + target: 'ethers-v5', }, networks: { localhost: { - url: "http://localhost:8545/", + url: 'http://localhost:8545/', saveDeployments: false, }, - goerli: getInfuraNetwork("goerli"), + goerli: getInfuraNetwork('goerli'), }, solidity: { - version: "0.8.15", + version: '0.8.15', settings: { optimizer: { enabled: true }, }, diff --git a/package.json b/package.json index ee573d0..17b525b 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,11 @@ "url": "https://github.com/MetaMask/test-bundler.git" }, "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "files": [ "dist" ], - "main": "./dist/index.js", - "types": "./dist/index.d.ts", "scripts": { "build": "tsc", "build:clean": "rm -rf dist tsconfig.tsbuildinfo && yarn build", @@ -71,12 +71,12 @@ "@types/express": "^4.17.13", "@types/mocha": "^9.1.0", "@types/node": "^16.4.12", - "@typescript-eslint/eslint-plugin": "^5.33.0", - "@typescript-eslint/parser": "^5.33.0", + "@typescript-eslint/eslint-plugin": "^5.43.0", + "@typescript-eslint/parser": "^5.43.0", "body-parser": "^1.20.0", "chai": "^4.2.0", "depcheck": "^1.4.3", - "eslint": "^8.21.0", + "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^27.2.2", diff --git a/src/bundler/BundlerConfig.ts b/src/bundler/BundlerConfig.ts index cbef8b1..5898ab3 100644 --- a/src/bundler/BundlerConfig.ts +++ b/src/bundler/BundlerConfig.ts @@ -1,28 +1,28 @@ // TODO: consider adopting config-loading approach from hardhat to allow code in config file -import ow from 'ow' +import ow from 'ow'; -const MIN_UNSTAKE_DELAY = 86400 -const MIN_STAKE_VALUE = 1e18.toString() -export interface BundlerConfig { - beneficiary: string - entryPoint: string - gasFactor: string - minBalance: string - mnemonic: string - network: string - port: string - unsafe: boolean - debugRpc?: boolean - conditionalRpc: boolean +const MIN_UNSTAKE_DELAY = 86400; +const MIN_STAKE_VALUE = (1e18).toString(); +export type BundlerConfig = { + beneficiary: string; + entryPoint: string; + gasFactor: string; + minBalance: string; + mnemonic: string; + network: string; + port: string; + unsafe: boolean; + debugRpc?: boolean; + conditionalRpc: boolean; - whitelist?: string[] - blacklist?: string[] - maxBundleGas: number - minStake: string - minUnstakeDelay: number - autoBundleInterval: number - autoBundleMempoolSize: number -} + whitelist?: string[]; + blacklist?: string[]; + maxBundleGas: number; + minStake: string; + minUnstakeDelay: number; + autoBundleInterval: number; + autoBundleMempoolSize: number; +}; // TODO: implement merging config (args -> config.js -> default) and runtime shape validation export const BundlerConfigShape = { @@ -43,8 +43,8 @@ export const BundlerConfigShape = { minStake: ow.string, minUnstakeDelay: ow.number, autoBundleInterval: ow.number, - autoBundleMempoolSize: ow.number -} + autoBundleMempoolSize: ow.number, +}; // TODO: consider if we want any default fields at all // TODO: implement merging config (args -> config.js -> default) and runtime shape validation @@ -54,5 +54,5 @@ export const bundlerConfigDefault: Partial = { unsafe: false, conditionalRpc: false, minStake: MIN_STAKE_VALUE, - minUnstakeDelay: MIN_UNSTAKE_DELAY -} + minUnstakeDelay: MIN_UNSTAKE_DELAY, +}; diff --git a/src/bundler/BundlerServer.ts b/src/bundler/BundlerServer.ts index 6b46b05..7d72caf 100644 --- a/src/bundler/BundlerServer.ts +++ b/src/bundler/BundlerServer.ts @@ -1,59 +1,66 @@ -import bodyParser from 'body-parser' -import cors from 'cors' -import express, { Express, Response, Request } from 'express' -import { Provider } from '@ethersproject/providers' -import { Signer, utils } from 'ethers' -import { parseEther } from 'ethers/lib/utils' - -import { AddressZero, deepHexlify, erc4337RuntimeVersion, RpcError } from '../utils' - -import { BundlerConfig } from './BundlerConfig' -import { UserOpMethodHandler } from './UserOpMethodHandler' -import { Server } from 'http' -import { EntryPoint__factory, UserOperationStruct } from '@account-abstraction/contracts' -import { DebugMethodHandler } from './DebugMethodHandler' - -import Debug from 'debug' - -const debug = Debug('aa.rpc') +import type { UserOperationStruct } from '@account-abstraction/contracts'; +import { EntryPoint__factory } from '@account-abstraction/contracts'; +import type { Provider } from '@ethersproject/providers'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import Debug from 'debug'; +import type { Signer } from 'ethers'; +import { utils } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; +import type { Express, Response, Request } from 'express'; +import express from 'express'; +import type { Server } from 'http'; + +import type { BundlerConfig } from './BundlerConfig'; +import type { DebugMethodHandler } from './DebugMethodHandler'; +import type { UserOpMethodHandler } from './UserOpMethodHandler'; +import { + AddressZero, + deepHexlify, + erc4337RuntimeVersion, + RpcError, +} from '../utils'; + +const debug = Debug('aa.rpc'); export class BundlerServer { - app: Express - private readonly httpServer: Server + app: Express; + + private readonly httpServer: Server; - constructor ( + constructor( readonly methodHandler: UserOpMethodHandler, readonly debugHandler: DebugMethodHandler, readonly config: BundlerConfig, readonly provider: Provider, - readonly wallet: Signer + readonly wallet: Signer, ) { - this.app = express() - this.app.use(cors()) - this.app.use(bodyParser.json()) + this.app = express(); + this.app.use(cors()); + this.app.use(bodyParser.json()); - this.app.get('/', this.intro.bind(this)) - this.app.post('/', this.intro.bind(this)) + this.app.get('/', this.intro.bind(this)); + this.app.post('/', this.intro.bind(this)); // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.app.post('/rpc', this.rpc.bind(this)) + this.app.post('/rpc', this.rpc.bind(this)); - this.httpServer = this.app.listen(this.config.port) - this.startingPromise = this._preflightCheck() + this.httpServer = this.app.listen(this.config.port); + this.startingPromise = this._preflightCheck(); } - startingPromise: Promise + startingPromise: Promise; - async asyncStart (): Promise { - await this.startingPromise + async asyncStart(): Promise { + await this.startingPromise; } - async stop (): Promise { - this.httpServer.close() + async stop(): Promise { + this.httpServer.close(); } - async _preflightCheck (): Promise { - if (await this.provider.getCode(this.config.entryPoint) === '0x') { - this.fatal(`entrypoint not deployed at ${this.config.entryPoint}`) + async _preflightCheck(): Promise { + if ((await this.provider.getCode(this.config.entryPoint)) === '0x') { + this.fatal(`entrypoint not deployed at ${this.config.entryPoint}`); } // minimal UserOp to revert with "FailedOp" @@ -68,157 +75,174 @@ export class BundlerServer { callGasLimit: 0, maxFeePerGas: 0, maxPriorityFeePerGas: 0, - signature: '0x' - } + signature: '0x', + }; // await EntryPoint__factory.connect(this.config.entryPoint,this.provider).callStatic.addStake(0) - const err = await EntryPoint__factory.connect(this.config.entryPoint, this.provider).callStatic.simulateValidation(emptyUserOp) - .catch(e => e) + const err = await EntryPoint__factory.connect( + this.config.entryPoint, + this.provider, + ) + .callStatic.simulateValidation(emptyUserOp) + .catch((e) => e); if (err?.errorName !== 'FailedOp') { - this.fatal(`Invalid entryPoint contract at ${this.config.entryPoint}. wrong version?`) + this.fatal( + `Invalid entryPoint contract at ${this.config.entryPoint}. wrong version?`, + ); } - const signerAddress = await this.wallet.getAddress() - const bal = await this.provider.getBalance(signerAddress) - console.log('signer', signerAddress, 'balance', utils.formatEther(bal)) + const signerAddress = await this.wallet.getAddress(); + const bal = await this.provider.getBalance(signerAddress); + console.log('signer', signerAddress, 'balance', utils.formatEther(bal)); if (bal.eq(0)) { - this.fatal('cannot run with zero balance') + this.fatal('cannot run with zero balance'); } else if (bal.lt(parseEther(this.config.minBalance))) { - console.log('WARNING: initial balance below --minBalance ', this.config.minBalance) + console.log( + 'WARNING: initial balance below --minBalance ', + this.config.minBalance, + ); } } - fatal (msg: string): never { - console.error('FATAL:', msg) - process.exit(1) + fatal(msg: string): never { + console.error('FATAL:', msg); + process.exit(1); } - intro (req: Request, res: Response): void { - res.send(`Account-Abstraction Bundler v.${erc4337RuntimeVersion}. please use "/rpc"`) + intro(req: Request, res: Response): void { + res.send( + `Account-Abstraction Bundler v.${erc4337RuntimeVersion}. please use "/rpc"`, + ); } - async rpc (req: Request, res: Response): Promise { - let resContent: any + async rpc(req: Request, res: Response): Promise { + let resContent: any; if (Array.isArray(req.body)) { - resContent = [] + resContent = []; for (const reqItem of req.body) { - resContent.push(await this.handleRpc(reqItem)) + resContent.push(await this.handleRpc(reqItem)); } } else { - resContent = await this.handleRpc(req.body) + resContent = await this.handleRpc(req.body); } try { - res.send(resContent) + res.send(resContent); } catch (err: any) { const error = { message: err.message, data: err.data, - code: err.code - } - console.log('failed: ', 'rpc::res.send()', 'error:', JSON.stringify(error)) + code: err.code, + }; + console.log( + 'failed: ', + 'rpc::res.send()', + 'error:', + JSON.stringify(error), + ); } } - async handleRpc (reqItem: any): Promise { - const { - method, - params, - jsonrpc, - id - } = reqItem - debug('>>', { jsonrpc, id, method, params }) + async handleRpc(reqItem: any): Promise { + const { method, params, jsonrpc, id } = reqItem; + debug('>>', { jsonrpc, id, method, params }); try { - const result = deepHexlify(await this.handleMethod(method, params)) - console.log('sent', method, '-', result) - debug('<<', { jsonrpc, id, result }) + const result = deepHexlify(await this.handleMethod(method, params)); + console.log('sent', method, '-', result); + debug('<<', { jsonrpc, id, result }); return { jsonrpc, id, - result - } + result, + }; } catch (err: any) { const error = { message: err.message, data: err.data, - code: err.code - } - console.log('failed: ', method, 'error:', JSON.stringify(error)) - debug('<<', { jsonrpc, id, error }) + code: err.code, + }; + console.log('failed: ', method, 'error:', JSON.stringify(error)); + debug('<<', { jsonrpc, id, error }); return { jsonrpc, id, - error - } + error, + }; } } - async handleMethod (method: string, params: any[]): Promise { - let result: any + async handleMethod(method: string, params: any[]): Promise { + let result: any; switch (method) { case 'eth_chainId': // eslint-disable-next-line no-case-declarations - const { chainId } = await this.provider.getNetwork() - result = chainId - break + const { chainId } = await this.provider.getNetwork(); + result = chainId; + break; case 'eth_supportedEntryPoints': - result = await this.methodHandler.getSupportedEntryPoints() - break + result = await this.methodHandler.getSupportedEntryPoints(); + break; case 'eth_sendUserOperation': - result = await this.methodHandler.sendUserOperation(params[0], params[1]) - break + result = await this.methodHandler.sendUserOperation( + params[0], + params[1], + ); + break; case 'eth_estimateUserOperationGas': - result = await this.methodHandler.estimateUserOperationGas(params[0], params[1]) - break + result = await this.methodHandler.estimateUserOperationGas( + params[0], + params[1], + ); + break; case 'eth_getUserOperationReceipt': - result = await this.methodHandler.getUserOperationReceipt(params[0]) - break + result = await this.methodHandler.getUserOperationReceipt(params[0]); + break; case 'eth_getUserOperationByHash': - result = await this.methodHandler.getUserOperationByHash(params[0]) - break + result = await this.methodHandler.getUserOperationByHash(params[0]); + break; case 'web3_clientVersion': - result = this.methodHandler.clientVersion() - break + result = this.methodHandler.clientVersion(); + break; case 'debug_bundler_clearState': - this.debugHandler.clearState() - result = 'ok' - break + this.debugHandler.clearState(); + result = 'ok'; + break; case 'debug_bundler_dumpMempool': - result = await this.debugHandler.dumpMempool() - break + result = await this.debugHandler.dumpMempool(); + break; case 'debug_bundler_clearMempool': - this.debugHandler.clearMempool() - result = 'ok' - break + this.debugHandler.clearMempool(); + result = 'ok'; + break; case 'debug_bundler_setReputation': - await this.debugHandler.setReputation(params[0]) - result = 'ok' - break + await this.debugHandler.setReputation(params[0]); + result = 'ok'; + break; case 'debug_bundler_dumpReputation': - result = await this.debugHandler.dumpReputation() - break + result = await this.debugHandler.dumpReputation(); + break; case 'debug_bundler_clearReputation': - this.debugHandler.clearReputation() - result = 'ok' - break + this.debugHandler.clearReputation(); + result = 'ok'; + break; case 'debug_bundler_setBundlingMode': - await this.debugHandler.setBundlingMode(params[0]) - result = 'ok' - break + await this.debugHandler.setBundlingMode(params[0]); + result = 'ok'; + break; case 'debug_bundler_setBundleInterval': - await this.debugHandler.setBundleInterval(params[0], params[1]) - result = 'ok' - break + await this.debugHandler.setBundleInterval(params[0], params[1]); + result = 'ok'; + break; case 'debug_bundler_sendBundleNow': - result = await this.debugHandler.sendBundleNow() + result = await this.debugHandler.sendBundleNow(); if (result == null) { - result = 'ok' + result = 'ok'; } - break + break; case 'debug_bundler_getStakeStatus': - result = await this.debugHandler.getStakeStatus(params[0], params[1]) - break + result = await this.debugHandler.getStakeStatus(params[0], params[1]); + break; default: - throw new RpcError(`Method ${method} is not supported`, -32601) + throw new RpcError(`Method ${method} is not supported`, -32601); } - return result + return result; } } diff --git a/src/bundler/Config.ts b/src/bundler/Config.ts index fb445d5..a15c04e 100644 --- a/src/bundler/Config.ts +++ b/src/bundler/Config.ts @@ -1,62 +1,89 @@ -import ow from 'ow' -import fs from 'fs' +import type { BaseProvider } from '@ethersproject/providers'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import type { Signer } from 'ethers'; +import { Wallet } from 'ethers'; +import fs from 'fs'; +import ow from 'ow'; -import { BundlerConfig, bundlerConfigDefault, BundlerConfigShape } from './BundlerConfig' -import { Wallet, Signer } from 'ethers' -import { BaseProvider, JsonRpcProvider } from '@ethersproject/providers' +import type { BundlerConfig } from './BundlerConfig'; +import { bundlerConfigDefault, BundlerConfigShape } from './BundlerConfig'; -function getCommandLineParams (programOpts: any): Partial { - const params: any = {} +/** + * + * @param programOpts + */ +function getCommandLineParams(programOpts: any): Partial { + const params: any = {}; for (const bundlerConfigShapeKey in BundlerConfigShape) { - const optionValue = programOpts[bundlerConfigShapeKey] + const optionValue = programOpts[bundlerConfigShapeKey]; if (optionValue != null) { - params[bundlerConfigShapeKey] = optionValue + params[bundlerConfigShapeKey] = optionValue; } } - return params as BundlerConfig + return params as BundlerConfig; } -function mergeConfigs (...sources: Array>): BundlerConfig { - const mergedConfig = Object.assign({}, ...sources) - ow(mergedConfig, ow.object.exactShape(BundlerConfigShape)) - return mergedConfig +/** + * + * @param sources + */ +function mergeConfigs(...sources: Partial[]): BundlerConfig { + const mergedConfig = Object.assign({}, ...sources); + ow(mergedConfig, ow.object.exactShape(BundlerConfigShape)); + return mergedConfig; } -const DEFAULT_INFURA_ID = 'd442d82a1ab34327a7126a578428dfc4' +const DEFAULT_INFURA_ID = 'd442d82a1ab34327a7126a578428dfc4'; -export function getNetworkProvider (url: string): JsonRpcProvider { +/** + * + * @param url + */ +export function getNetworkProvider(url: string): JsonRpcProvider { if (url.match(/^[\w-]+$/) != null) { - const infuraId = process.env.INFURA_ID1 ?? DEFAULT_INFURA_ID - url = `https://${url}.infura.io/v3/${infuraId}` + const infuraId = process.env.INFURA_ID1 ?? DEFAULT_INFURA_ID; + url = `https://${url}.infura.io/v3/${infuraId}`; } - console.log('url=', url) - return new JsonRpcProvider(url) + console.log('url=', url); + return new JsonRpcProvider(url); } -export async function resolveConfiguration (programOpts: any): Promise<{ config: BundlerConfig, provider: BaseProvider, wallet: Signer }> { - const commandLineParams = getCommandLineParams(programOpts) - let fileConfig: Partial = {} - const configFileName = programOpts.config +/** + * + * @param programOpts + */ +export async function resolveConfiguration( + programOpts: any, +): Promise<{ config: BundlerConfig; provider: BaseProvider; wallet: Signer }> { + const commandLineParams = getCommandLineParams(programOpts); + let fileConfig: Partial = {}; + const configFileName = programOpts.config; if (fs.existsSync(configFileName)) { - fileConfig = JSON.parse(fs.readFileSync(configFileName, 'ascii')) + fileConfig = JSON.parse(fs.readFileSync(configFileName, 'ascii')); } - const config = mergeConfigs(bundlerConfigDefault, fileConfig, commandLineParams) - console.log('Merged configuration:', JSON.stringify(config)) + const config = mergeConfigs( + bundlerConfigDefault, + fileConfig, + commandLineParams, + ); + console.log('Merged configuration:', JSON.stringify(config)); if (config.network === 'hardhat') { // eslint-disable-next-line const provider: JsonRpcProvider = require('hardhat').ethers.provider - return { config, provider, wallet: provider.getSigner() } + return { config, provider, wallet: provider.getSigner() }; } - const provider: BaseProvider = getNetworkProvider(config.network) - let mnemonic: string - let wallet: Wallet + const provider: BaseProvider = getNetworkProvider(config.network); + let mnemonic: string; + let wallet: Wallet; try { - mnemonic = fs.readFileSync(config.mnemonic, 'ascii').trim() - wallet = Wallet.fromMnemonic(mnemonic).connect(provider) + mnemonic = fs.readFileSync(config.mnemonic, 'ascii').trim(); + wallet = Wallet.fromMnemonic(mnemonic).connect(provider); } catch (e: any) { - throw new Error(`Unable to read --mnemonic ${config.mnemonic}: ${e.message as string}`) + throw new Error( + `Unable to read --mnemonic ${config.mnemonic}: ${e.message as string}`, + ); } - return { config, provider, wallet } + return { config, provider, wallet }; } diff --git a/src/bundler/DebugMethodHandler.ts b/src/bundler/DebugMethodHandler.ts index 6881c65..7f45508 100644 --- a/src/bundler/DebugMethodHandler.ts +++ b/src/bundler/DebugMethodHandler.ts @@ -1,79 +1,84 @@ -import { ExecutionManager } from './modules/ExecutionManager' -import { ReputationDump, ReputationManager } from './modules/ReputationManager' -import { MempoolManager } from './modules/MempoolManager' -import { SendBundleReturn } from './modules/BundleManager' -import { EventsManager } from './modules/EventsManager' -import { StakeInfo } from '../utils' +import type { SendBundleReturn } from './modules/BundleManager'; +import type { EventsManager } from './modules/EventsManager'; +import type { ExecutionManager } from './modules/ExecutionManager'; +import type { MempoolManager } from './modules/MempoolManager'; +import type { + ReputationDump, + ReputationManager, +} from './modules/ReputationManager'; +import type { StakeInfo } from '../utils'; export class DebugMethodHandler { - constructor ( + constructor( readonly execManager: ExecutionManager, readonly eventsManager: EventsManager, readonly repManager: ReputationManager, - readonly mempoolMgr: MempoolManager - ) { - } + readonly mempoolMgr: MempoolManager, + ) {} - setBundlingMode (mode: 'manual' | 'auto'): void { - this.setBundleInterval(mode) + setBundlingMode(mode: 'manual' | 'auto'): void { + this.setBundleInterval(mode); } - setBundleInterval (interval: number | 'manual' | 'auto', maxPoolSize = 100): void { + setBundleInterval( + interval: number | 'manual' | 'auto', + maxPoolSize = 100, + ): void { if (interval == null) { - throw new Error('must specify interval |manual|auto') + throw new Error('must specify interval |manual|auto'); } if (interval === 'auto') { // size=0 ==> auto-bundle on each userop - this.execManager.setAutoBundler(0, 0) + this.execManager.setAutoBundler(0, 0); } else if (interval === 'manual') { // interval=0, but never auto-mine - this.execManager.setAutoBundler(0, 1000) + this.execManager.setAutoBundler(0, 1000); } else { - this.execManager.setAutoBundler(interval, maxPoolSize) + this.execManager.setAutoBundler(interval, maxPoolSize); } } - async sendBundleNow (): Promise { - const ret = await this.execManager.attemptBundle(true) + async sendBundleNow(): Promise { + const ret = await this.execManager.attemptBundle(true); // handlePastEvents is performed before processing the next bundle. // however, in debug mode, we are interested in the side effects // (on the mempool) of this "sendBundle" operation - await this.eventsManager.handlePastEvents() - return ret + await this.eventsManager.handlePastEvents(); + return ret; } - clearState (): void { - this.mempoolMgr.clearState() - this.repManager.clearState() + clearState(): void { + this.mempoolMgr.clearState(); + this.repManager.clearState(); } - async dumpMempool (): Promise { - return this.mempoolMgr.dump() + async dumpMempool(): Promise { + return this.mempoolMgr.dump(); } - clearMempool (): void { - this.mempoolMgr.clearState() + clearMempool(): void { + this.mempoolMgr.clearState(); } - setReputation (param: any): ReputationDump { - return this.repManager.setReputation(param) + setReputation(param: any): ReputationDump { + return this.repManager.setReputation(param); } - dumpReputation (): ReputationDump { - return this.repManager.dump() + dumpReputation(): ReputationDump { + return this.repManager.dump(); } - clearReputation (): void { - this.repManager.clearState() + clearReputation(): void { + this.repManager.clearState(); } - async getStakeStatus ( + async getStakeStatus( address: string, - entryPoint: string + entryPoint: string, ): Promise<{ - stakeInfo: StakeInfo - isStaked: boolean - }> { - return await this.repManager.getStakeStatus(address, entryPoint) + stakeInfo: StakeInfo; + isStaked: boolean; + }> { + return await this.repManager.getStakeStatus(address, entryPoint); } } diff --git a/src/bundler/RpcTypes.ts b/src/bundler/RpcTypes.ts index cbc73bd..4723d78 100644 --- a/src/bundler/RpcTypes.ts +++ b/src/bundler/RpcTypes.ts @@ -1,6 +1,7 @@ -import { BigNumberish } from 'ethers' -import { TransactionReceipt } from '@ethersproject/providers' -import { UserOperation } from '../utils' +import type { TransactionReceipt } from '@ethersproject/providers'; +import type { BigNumberish } from 'ethers'; + +import type { UserOperation } from '../utils'; /** * RPC calls return types @@ -9,53 +10,53 @@ import { UserOperation } from '../utils' /** * return value from estimateUserOpGas */ -export interface EstimateUserOpGasResult { +export type EstimateUserOpGasResult = { /** * the preVerification gas used by this UserOperation. */ - preVerificationGas: BigNumberish + preVerificationGas: BigNumberish; /** * gas used for validation of this UserOperation, including account creation */ - verificationGasLimit: BigNumberish + verificationGasLimit: BigNumberish; /** * the deadline after which this UserOperation is invalid (not a gas estimation parameter, but returned by validation */ - deadline?: BigNumberish + deadline?: BigNumberish; /** * estimated cost of calling the account with the given callData */ - callGasLimit: BigNumberish -} + callGasLimit: BigNumberish; +}; -export interface UserOperationByHashResponse { - userOperation: UserOperation - entryPoint: string - blockNumber: number - blockHash: string - transactionHash: string -} +export type UserOperationByHashResponse = { + userOperation: UserOperation; + entryPoint: string; + blockNumber: number; + blockHash: string; + transactionHash: string; +}; -export interface UserOperationReceipt { - /// the request hash - userOpHash: string - /// the account sending this UserOperation - sender: string - /// account nonce - nonce: BigNumberish - /// the paymaster used for this userOp (or empty) - paymaster?: string - /// actual payment for this UserOperation (by either paymaster or account) - actualGasCost: BigNumberish - /// total gas used by this UserOperation (including preVerification, creation, validation and execution) - actualGasUsed: BigNumberish - /// did this execution completed without revert - success: boolean - /// in case of revert, this is the revert reason - reason?: string - /// the logs generated by this UserOperation (not including logs of other UserOperations in the same bundle) - logs: any[] +export type UserOperationReceipt = { + // / the request hash + userOpHash: string; + // / the account sending this UserOperation + sender: string; + // / account nonce + nonce: BigNumberish; + // / the paymaster used for this userOp (or empty) + paymaster?: string; + // / actual payment for this UserOperation (by either paymaster or account) + actualGasCost: BigNumberish; + // / total gas used by this UserOperation (including preVerification, creation, validation and execution) + actualGasUsed: BigNumberish; + // / did this execution completed without revert + success: boolean; + // / in case of revert, this is the revert reason + reason?: string; + // / the logs generated by this UserOperation (not including logs of other UserOperations in the same bundle) + logs: any[]; // the transaction receipt for this transaction (of entire bundle, not only this UserOperation) - receipt: TransactionReceipt -} + receipt: TransactionReceipt; +}; diff --git a/src/bundler/UserOpMethodHandler.ts b/src/bundler/UserOpMethodHandler.ts index 71a7b2c..96448a7 100644 --- a/src/bundler/UserOpMethodHandler.ts +++ b/src/bundler/UserOpMethodHandler.ts @@ -1,100 +1,155 @@ -import { BigNumber, BigNumberish, Signer } from 'ethers' -import { Log, Provider } from '@ethersproject/providers' +import type { + UserOperationStruct, + EntryPoint, +} from '@account-abstraction/contracts'; +import type { UserOperationEventEvent } from '@account-abstraction/contracts/dist/types/EntryPoint'; +import type { Log, Provider } from '@ethersproject/providers'; +import { BigNumber } from 'ethers'; +import type { BigNumberish, Signer } from 'ethers'; +import { resolveProperties } from 'ethers/lib/utils'; -import { BundlerConfig } from './BundlerConfig' -import { resolveProperties } from 'ethers/lib/utils' -import { UserOperation, deepHexlify, erc4337RuntimeVersion, requireCond, RpcError, tostr, getAddr, ValidationErrors } from '../utils' -import { UserOperationStruct, EntryPoint } from '@account-abstraction/contracts' -import { UserOperationEventEvent } from '@account-abstraction/contracts/dist/types/EntryPoint' -import { calcPreVerificationGas } from '../sdk' -import { ExecutionManager } from './modules/ExecutionManager' -import { UserOperationByHashResponse, UserOperationReceipt } from './RpcTypes' +import type { BundlerConfig } from './BundlerConfig'; +import type { ExecutionManager } from './modules/ExecutionManager'; +import type { + UserOperationByHashResponse, + UserOperationReceipt, +} from './RpcTypes'; +import { calcPreVerificationGas } from '../sdk'; +import { + deepHexlify, + erc4337RuntimeVersion, + requireCond, + RpcError, + tostr, + getAddr, + ValidationErrors, +} from '../utils'; +import type { UserOperation } from '../utils'; -const HEX_REGEX = /^0x[a-fA-F\d]*$/i +const HEX_REGEX = /^0x[a-fA-F\d]*$/i; /** * return value from estimateUserOpGas */ -export interface EstimateUserOpGasResult { +export type EstimateUserOpGasResult = { /** * the preVerification gas used by this UserOperation. */ - preVerificationGas: BigNumberish + preVerificationGas: BigNumberish; /** * gas used for validation of this UserOperation, including account creation */ - verificationGasLimit: BigNumberish + verificationGasLimit: BigNumberish; /** * (possibly future timestamp) after which this UserOperation is valid */ - validAfter?: BigNumberish + validAfter?: BigNumberish; /** * the deadline after which this UserOperation is invalid (not a gas estimation parameter, but returned by validation */ - validUntil?: BigNumberish + validUntil?: BigNumberish; /** * estimated cost of calling the account with the given callData */ - callGasLimit: BigNumberish -} + callGasLimit: BigNumberish; +}; export class UserOpMethodHandler { - constructor ( + constructor( readonly execManager: ExecutionManager, readonly provider: Provider, readonly signer: Signer, readonly config: BundlerConfig, - readonly entryPoint: EntryPoint - ) { - } + readonly entryPoint: EntryPoint, + ) {} - async getSupportedEntryPoints (): Promise { - return [this.config.entryPoint] + async getSupportedEntryPoints(): Promise { + return [this.config.entryPoint]; } - async selectBeneficiary (): Promise { - const currentBalance = await this.provider.getBalance(this.signer.getAddress()) - let beneficiary = this.config.beneficiary + async selectBeneficiary(): Promise { + const currentBalance = await this.provider.getBalance( + this.signer.getAddress(), + ); + let { beneficiary } = this.config; // below min-balance redeem to the signer, to keep it active. if (currentBalance.lte(this.config.minBalance)) { - beneficiary = await this.signer.getAddress() - console.log('low balance. using ', beneficiary, 'as beneficiary instead of ', this.config.beneficiary) + beneficiary = await this.signer.getAddress(); + console.log( + 'low balance. using ', + beneficiary, + 'as beneficiary instead of ', + this.config.beneficiary, + ); } - return beneficiary + return beneficiary; } - async _validateParameters (userOp1: UserOperationStruct, entryPointInput: string, requireSignature = true, requireGasParams = true): Promise { - requireCond(entryPointInput != null, 'No entryPoint param', -32602) + async _validateParameters( + userOp1: UserOperationStruct, + entryPointInput: string, + requireSignature = true, + requireGasParams = true, + ): Promise { + requireCond(entryPointInput != null, 'No entryPoint param', -32602); - if (entryPointInput?.toString().toLowerCase() !== this.config.entryPoint.toLowerCase()) { - throw new Error(`The EntryPoint at "${entryPointInput}" is not supported. This bundler uses ${this.config.entryPoint}`) + if ( + entryPointInput?.toString().toLowerCase() !== + this.config.entryPoint.toLowerCase() + ) { + throw new Error( + `The EntryPoint at "${entryPointInput}" is not supported. This bundler uses ${this.config.entryPoint}`, + ); } // minimal sanity check: userOp exists, and all members are hex - requireCond(userOp1 != null, 'No UserOperation param') - const userOp = await resolveProperties(userOp1) as any + requireCond(userOp1 != null, 'No UserOperation param'); + const userOp = (await resolveProperties(userOp1)) as any; - const fields = ['sender', 'nonce', 'initCode', 'callData', 'paymasterAndData'] + const fields = [ + 'sender', + 'nonce', + 'initCode', + 'callData', + 'paymasterAndData', + ]; if (requireSignature) { - fields.push('signature') + fields.push('signature'); } if (requireGasParams) { - fields.push('preVerificationGas', 'verificationGasLimit', 'callGasLimit', 'maxFeePerGas', 'maxPriorityFeePerGas') + fields.push( + 'preVerificationGas', + 'verificationGasLimit', + 'callGasLimit', + 'maxFeePerGas', + 'maxPriorityFeePerGas', + ); } - fields.forEach(key => { - requireCond(userOp[key] != null, 'Missing userOp field: ' + key + JSON.stringify(userOp), -32602) - const value: string = userOp[key].toString() - requireCond(value.match(HEX_REGEX) != null, `Invalid hex value for property ${key}:${value} in UserOp`, -32602) - }) + fields.forEach((key) => { + requireCond( + userOp[key] != null, + `Missing userOp field: ${key}${JSON.stringify(userOp)}`, + -32602, + ); + const value: string = userOp[key].toString(); + requireCond( + value.match(HEX_REGEX) != null, + `Invalid hex value for property ${key}:${value} in UserOp`, + -32602, + ); + }); } /** * eth_estimateUserOperationGas RPC api. - * @param userOp1 input userOp (may have gas fields missing, so they can be estimated) + * @param userOp1 - input userOp (may have gas fields missing, so they can be estimated) * @param entryPointInput */ - async estimateUserOperationGas (userOp1: UserOperationStruct, entryPointInput: string): Promise { + async estimateUserOperationGas( + userOp1: UserOperationStruct, + entryPointInput: string, + ): Promise { const userOp = { // default values for missing fields. paymasterAndData: '0x', @@ -102,125 +157,151 @@ export class UserOpMethodHandler { maxPriorityFeePerGas: 0, preVerificationGas: 0, verificationGasLimit: 10e6, - ...await resolveProperties(userOp1) as any - } + ...((await resolveProperties(userOp1)) as any), + }; // todo: checks the existence of parameters, but since we hexlify the inputs, it fails to validate - await this._validateParameters(deepHexlify(userOp), entryPointInput) + await this._validateParameters(deepHexlify(userOp), entryPointInput); // todo: validation manager duplicate? - const errorResult = await this.entryPoint.callStatic.simulateValidation(userOp).catch(e => e) + const errorResult = await this.entryPoint.callStatic + .simulateValidation(userOp) + .catch((e) => e); if (errorResult.errorName === 'FailedOp') { - throw new RpcError(errorResult.errorArgs.at(-1), ValidationErrors.SimulateValidation) + throw new RpcError( + errorResult.errorArgs.at(-1), + ValidationErrors.SimulateValidation, + ); } // todo throw valid rpc error if (errorResult.errorName !== 'ValidationResult') { - throw errorResult + throw errorResult; } - const { returnInfo } = errorResult.errorArgs - let { - preOpGas, - validAfter, - validUntil - } = returnInfo + const { returnInfo } = errorResult.errorArgs; + let { preOpGas, validAfter, validUntil } = returnInfo; - const callGasLimit = await this.provider.estimateGas({ - from: this.entryPoint.address, - to: userOp.sender, - data: userOp.callData - }).then(b => b.toNumber()).catch(err => { - const message = err.message.match(/reason="(.*?)"/)?.at(1) ?? 'execution reverted' - throw new RpcError(message, ValidationErrors.UserOperationReverted) - }) - validAfter = BigNumber.from(validAfter) - validUntil = BigNumber.from(validUntil) + const callGasLimit = await this.provider + .estimateGas({ + from: this.entryPoint.address, + to: userOp.sender, + data: userOp.callData, + }) + .then((b) => b.toNumber()) + .catch((err) => { + const message = + err.message.match(/reason="(.*?)"/)?.at(1) ?? 'execution reverted'; + throw new RpcError(message, ValidationErrors.UserOperationReverted); + }); + validAfter = BigNumber.from(validAfter); + validUntil = BigNumber.from(validUntil); if ((validUntil as BigNumber).eq(0)) { - validUntil = undefined + validUntil = undefined; } if ((validAfter as BigNumber).eq(0)) { - validAfter = undefined + validAfter = undefined; } - const preVerificationGas = calcPreVerificationGas(userOp) - const verificationGasLimit = BigNumber.from(preOpGas).toNumber() + const preVerificationGas = calcPreVerificationGas(userOp); + const verificationGasLimit = BigNumber.from(preOpGas).toNumber(); return { preVerificationGas, verificationGasLimit, validAfter, validUntil, - callGasLimit - } + callGasLimit, + }; } - async sendUserOperation (userOp1: UserOperationStruct, entryPointInput: string): Promise { - await this._validateParameters(userOp1, entryPointInput) + async sendUserOperation( + userOp1: UserOperationStruct, + entryPointInput: string, + ): Promise { + await this._validateParameters(userOp1, entryPointInput); - const userOp = await resolveProperties(userOp1) + const userOp = await resolveProperties(userOp1); - console.log(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${getAddr( - userOp.paymasterAndData)}`) - await this.execManager.sendUserOperation(userOp, entryPointInput) - return await this.entryPoint.getUserOpHash(userOp) + console.log( + `UserOperation: Sender=${userOp.sender} Nonce=${tostr( + userOp.nonce, + )} EntryPoint=${entryPointInput} Paymaster=${getAddr( + userOp.paymasterAndData, + )}`, + ); + await this.execManager.sendUserOperation(userOp, entryPointInput); + return await this.entryPoint.getUserOpHash(userOp); } - async _getUserOperationEvent (userOpHash: string): Promise { + async _getUserOperationEvent( + userOpHash: string, + ): Promise { // TODO: eth_getLogs is throttled. must be acceptable for finding a UserOperation by hash - const event = await this.entryPoint.queryFilter(this.entryPoint.filters.UserOperationEvent(userOpHash)) - return event[0] + const event = await this.entryPoint.queryFilter( + this.entryPoint.filters.UserOperationEvent(userOpHash), + ); + return event[0]; } // filter full bundle logs, and leave only logs for the given userOpHash // @param userOpEvent - the event of our UserOp (known to exist in the logs) // @param logs - full bundle logs. after each group of logs there is a single UserOperationEvent with unique hash. - _filterLogs (userOpEvent: UserOperationEventEvent, logs: Log[]): Log[] { - let startIndex = -1 - let endIndex = -1 - const events = Object.values(this.entryPoint.interface.events) + _filterLogs(userOpEvent: UserOperationEventEvent, logs: Log[]): Log[] { + let startIndex = -1; + let endIndex = -1; + const events = Object.values(this.entryPoint.interface.events); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const beforeExecutionTopic = this.entryPoint.interface.getEventTopic(events.find(e => e.name === 'BeforeExecution')!) + const beforeExecutionTopic = this.entryPoint.interface.getEventTopic( + events.find((e) => e.name === 'BeforeExecution')!, + ); logs.forEach((log, index) => { if (log?.topics[0] === beforeExecutionTopic) { // all UserOp execution events start after the "BeforeExecution" event. - startIndex = endIndex = index + startIndex = endIndex = index; } else if (log?.topics[0] === userOpEvent.topics[0]) { // process UserOperationEvent if (log.topics[1] === userOpEvent.topics[1]) { // it's our userOpHash. save as end of logs array - endIndex = index + endIndex = index; } else { // it's a different hash. remember it as beginning index, but only if we didn't find our end index yet. if (endIndex === -1) { - startIndex = index + startIndex = index; } } } - }) + }); if (endIndex === -1) { - throw new Error('fatal: no UserOperationEvent in logs') + throw new Error('fatal: no UserOperationEvent in logs'); } - return logs.slice(startIndex + 1, endIndex) + return logs.slice(startIndex + 1, endIndex); } - async getUserOperationByHash (userOpHash: string): Promise { - requireCond(userOpHash?.toString()?.match(HEX_REGEX) != null, 'Missing/invalid userOpHash', -32601) - const event = await this._getUserOperationEvent(userOpHash) + async getUserOperationByHash( + userOpHash: string, + ): Promise { + requireCond( + userOpHash?.toString()?.match(HEX_REGEX) != null, + 'Missing/invalid userOpHash', + -32601, + ); + const event = await this._getUserOperationEvent(userOpHash); if (event == null) { - return null + return null; } - const tx = await event.getTransaction() + const tx = await event.getTransaction(); if (tx.to !== this.entryPoint.address) { - throw new Error('unable to parse transaction') + throw new Error('unable to parse transaction'); } - const parsed = this.entryPoint.interface.parseTransaction(tx) - const ops: UserOperation[] = parsed?.args.ops + const parsed = this.entryPoint.interface.parseTransaction(tx); + const ops: UserOperation[] = parsed?.args.ops; if (ops == null) { - throw new Error('failed to parse transaction') + throw new Error('failed to parse transaction'); } - const op = ops.find(op => - op.sender === event.args.sender && - BigNumber.from(op.nonce).eq(event.args.nonce) - ) + const op = ops.find( + (op) => + op.sender === event.args.sender && + BigNumber.from(op.nonce).eq(event.args.nonce), + ); if (op == null) { - throw new Error('unable to find userOp in transaction') + throw new Error('unable to find userOp in transaction'); } const { @@ -234,8 +315,8 @@ export class UserOpMethodHandler { maxFeePerGas, maxPriorityFeePerGas, paymasterAndData, - signature - } = op + signature, + } = op; return deepHexlify({ userOperation: { @@ -249,23 +330,29 @@ export class UserOpMethodHandler { maxFeePerGas, maxPriorityFeePerGas, paymasterAndData, - signature + signature, }, entryPoint: this.entryPoint.address, transactionHash: tx.hash, blockHash: tx.blockHash ?? '', - blockNumber: tx.blockNumber ?? 0 - }) + blockNumber: tx.blockNumber ?? 0, + }); } - async getUserOperationReceipt (userOpHash: string): Promise { - requireCond(userOpHash?.toString()?.match(HEX_REGEX) != null, 'Missing/invalid userOpHash', -32601) - const event = await this._getUserOperationEvent(userOpHash) + async getUserOperationReceipt( + userOpHash: string, + ): Promise { + requireCond( + userOpHash?.toString()?.match(HEX_REGEX) != null, + 'Missing/invalid userOpHash', + -32601, + ); + const event = await this._getUserOperationEvent(userOpHash); if (event == null) { - return null + return null; } - const receipt = await event.getTransactionReceipt() - const logs = this._filterLogs(event, receipt.logs) + const receipt = await event.getTransactionReceipt(); + const logs = this._filterLogs(event, receipt.logs); return deepHexlify({ userOpHash, sender: event.args.sender, @@ -274,11 +361,11 @@ export class UserOpMethodHandler { actualGasUsed: event.args.actualGasUsed, success: event.args.success, logs, - receipt - }) + receipt, + }); } - clientVersion (): string { + clientVersion(): string { // eslint-disable-next-line return 'aa-bundler/' + erc4337RuntimeVersion + (this.config.unsafe ? '/unsafe' : '') } diff --git a/src/bundler/exec.ts b/src/bundler/exec.ts index 8bfc4c3..d6711dc 100644 --- a/src/bundler/exec.ts +++ b/src/bundler/exec.ts @@ -1,19 +1,20 @@ -import { runBundler, showStackTraces } from './runBundler' +import { runBundler, showStackTraces } from './runBundler'; process.on('SIGINT', () => { - process.exit(0) -}) + process.exit(0); +}); process.on('SIGTERM', () => { - process.exit(0) -}) + process.exit(0); +}); -void runBundler(process.argv).then((bundler) => { - process.on('exit', () => { - void bundler.stop() - }) -}) - .catch(e => { - console.error('Aborted:', showStackTraces ? e : e.message) - process.exit(1) +void runBundler(process.argv) + .then((bundler) => { + process.on('exit', () => { + void bundler.stop(); + }); }) + .catch((e) => { + console.error('Aborted:', showStackTraces ? e : e.message); + process.exit(1); + }); diff --git a/src/bundler/modules/BundleManager.ts b/src/bundler/modules/BundleManager.ts index 7cdcf4e..2d295dc 100644 --- a/src/bundler/modules/BundleManager.ts +++ b/src/bundler/modules/BundleManager.ts @@ -1,31 +1,40 @@ -import { EntryPoint } from '@account-abstraction/contracts' -import { MempoolManager } from './MempoolManager' -import { ValidateUserOpResult, ValidationManager } from '../../validation-manager' -import { BigNumber, BigNumberish } from 'ethers' -import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers' -import Debug from 'debug' -import { ReputationManager, ReputationStatus } from './ReputationManager' -import { Mutex } from 'async-mutex' -import { GetUserOpHashes__factory } from '../../contract-types' -import { UserOperation, StorageMap, getAddr, mergeStorageMap, runContractScript } from '../../utils' -import { EventsManager } from './EventsManager' -import { ErrorDescription } from '@ethersproject/abi/lib/interface' +import type { EntryPoint } from '@account-abstraction/contracts'; +import type { ErrorDescription } from '@ethersproject/abi/lib/interface'; +import type { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; +import { Mutex } from 'async-mutex'; +import Debug from 'debug'; +import type { BigNumberish } from 'ethers'; +import { BigNumber } from 'ethers'; -const debug = Debug('aa.exec.cron') +import type { EventsManager } from './EventsManager'; +import type { MempoolManager } from './MempoolManager'; +import type { ReputationManager } from './ReputationManager'; +import { ReputationStatus } from './ReputationManager'; +import { GetUserOpHashes__factory } from '../../contract-types'; +import type { UserOperation, StorageMap } from '../../utils'; +import { getAddr, mergeStorageMap, runContractScript } from '../../utils'; +import type { + ValidateUserOpResult, + ValidationManager, +} from '../../validation-manager'; -const THROTTLED_ENTITY_BUNDLE_COUNT = 4 +const debug = Debug('aa.exec.cron'); -export interface SendBundleReturn { - transactionHash: string - userOpHashes: string[] -} +const THROTTLED_ENTITY_BUNDLE_COUNT = 4; + +export type SendBundleReturn = { + transactionHash: string; + userOpHashes: string[]; +}; export class BundleManager { - provider: JsonRpcProvider - signer: JsonRpcSigner - mutex = new Mutex() + provider: JsonRpcProvider; + + signer: JsonRpcSigner; + + mutex = new Mutex(); - constructor ( + constructor( readonly entryPoint: EntryPoint, readonly eventsManager: EventsManager, readonly mempoolManager: MempoolManager, @@ -37,10 +46,10 @@ export class BundleManager { // use eth_sendRawTransactionConditional with storage map readonly conditionalRpc: boolean, // in conditionalRpc: always put root hash (not specific storage slots) for "sender" entries - readonly mergeToAccountRootHash: boolean = false + readonly mergeToAccountRootHash: boolean = false, ) { - this.provider = entryPoint.provider as JsonRpcProvider - this.signer = entryPoint.signer as JsonRpcSigner + this.provider = entryPoint.provider as JsonRpcProvider; + this.signer = entryPoint.signer as JsonRpcSigner; } /** @@ -48,155 +57,200 @@ export class BundleManager { * collect UserOps from mempool into a bundle * send this bundle. */ - async sendNextBundle (): Promise { + async sendNextBundle(): Promise { return await this.mutex.runExclusive(async () => { - debug('sendNextBundle') + debug('sendNextBundle'); // first flush mempool from already-included UserOps, by actively scanning past events. - await this.handlePastEvents() + await this.handlePastEvents(); - const [bundle, storageMap] = await this.createBundle() + const [bundle, storageMap] = await this.createBundle(); if (bundle.length === 0) { - debug('sendNextBundle - no bundle to send') + debug('sendNextBundle - no bundle to send'); } else { - const beneficiary = await this._selectBeneficiary() - const ret = await this.sendBundle(bundle, beneficiary, storageMap) - debug(`sendNextBundle exit - after sent a bundle of ${bundle.length} `) - return ret + const beneficiary = await this._selectBeneficiary(); + const ret = await this.sendBundle(bundle, beneficiary, storageMap); + debug(`sendNextBundle exit - after sent a bundle of ${bundle.length} `); + return ret; } - }) + }); } - async handlePastEvents (): Promise { - await this.eventsManager.handlePastEvents() + async handlePastEvents(): Promise { + await this.eventsManager.handlePastEvents(); } /** * submit a bundle. * after submitting the bundle, remove all UserOps from the mempool - * @return SendBundleReturn the transaction and UserOp hashes on successful transaction, or null on failed transaction + * @param userOps + * @param beneficiary + * @param storageMap + * @returns SendBundleReturn the transaction and UserOp hashes on successful transaction, or null on failed transaction */ - async sendBundle (userOps: UserOperation[], beneficiary: string, storageMap: StorageMap): Promise { + async sendBundle( + userOps: UserOperation[], + beneficiary: string, + storageMap: StorageMap, + ): Promise { try { - const feeData = await this.provider.getFeeData() - const tx = await this.entryPoint.populateTransaction.handleOps(userOps, beneficiary, { - type: 2, - nonce: await this.signer.getTransactionCount(), - gasLimit: 10e6, - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0, - maxFeePerGas: feeData.maxFeePerGas ?? 0 - }) - tx.chainId = this.provider._network.chainId - const signedTx = await this.signer.signTransaction(tx) - let ret: string + const feeData = await this.provider.getFeeData(); + const tx = await this.entryPoint.populateTransaction.handleOps( + userOps, + beneficiary, + { + type: 2, + nonce: await this.signer.getTransactionCount(), + gasLimit: 10e6, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0, + maxFeePerGas: feeData.maxFeePerGas ?? 0, + }, + ); + tx.chainId = this.provider._network.chainId; + const signedTx = await this.signer.signTransaction(tx); + let ret: string; if (this.conditionalRpc) { - debug('eth_sendRawTransactionConditional', storageMap) + debug('eth_sendRawTransactionConditional', storageMap); ret = await this.provider.send('eth_sendRawTransactionConditional', [ - signedTx, { knownAccounts: storageMap } - ]) - debug('eth_sendRawTransactionConditional ret=', ret) + signedTx, + { knownAccounts: storageMap }, + ]); + debug('eth_sendRawTransactionConditional ret=', ret); } else { // ret = await this.signer.sendTransaction(tx) - ret = await this.provider.send('eth_sendRawTransaction', [signedTx]) - debug('eth_sendRawTransaction ret=', ret) + ret = await this.provider.send('eth_sendRawTransaction', [signedTx]); + debug('eth_sendRawTransaction ret=', ret); } // TODO: parse ret, and revert if needed. - debug('ret=', ret) - debug('sent handleOps with', userOps.length, 'ops. removing from mempool') + debug('ret=', ret); + debug( + 'sent handleOps with', + userOps.length, + 'ops. removing from mempool', + ); // hashes are needed for debug rpc only. - const hashes = await this.getUserOpHashes(userOps) + const hashes = await this.getUserOpHashes(userOps); return { transactionHash: ret, - userOpHashes: hashes - } + userOpHashes: hashes, + }; } catch (e: any) { - let parsedError: ErrorDescription + let parsedError: ErrorDescription; try { - parsedError = this.entryPoint.interface.parseError((e.data?.data ?? e.data)) + parsedError = this.entryPoint.interface.parseError( + e.data?.data ?? e.data, + ); } catch (e1) { - this.checkFatal(e) - console.warn('Failed handleOps, but non-FailedOp error', e) - return + this.checkFatal(e); + console.warn('Failed handleOps, but non-FailedOp error', e); + return; } - const { - opIndex, - reason - } = parsedError.args - const userOp = userOps[opIndex] - const reasonStr: string = reason.toString() + const { opIndex, reason } = parsedError.args; + const userOp = userOps[opIndex]; + const reasonStr: string = reason.toString(); if (reasonStr.startsWith('AA3')) { - this.reputationManager.crashedHandleOps(getAddr(userOp.paymasterAndData)) + this.reputationManager.crashedHandleOps( + getAddr(userOp.paymasterAndData), + ); } else if (reasonStr.startsWith('AA2')) { - this.reputationManager.crashedHandleOps(userOp.sender) + this.reputationManager.crashedHandleOps(userOp.sender); } else if (reasonStr.startsWith('AA1')) { - this.reputationManager.crashedHandleOps(getAddr(userOp.initCode)) + this.reputationManager.crashedHandleOps(getAddr(userOp.initCode)); } else { - this.mempoolManager.removeUserOp(userOp) - console.warn(`Failed handleOps sender=${userOp.sender} reason=${reasonStr}`) + this.mempoolManager.removeUserOp(userOp); + console.warn( + `Failed handleOps sender=${userOp.sender} reason=${reasonStr}`, + ); } } } // fatal errors we know we can't recover - checkFatal (e: any): void { + checkFatal(e: any): void { // console.log('ex entries=',Object.entries(e)) if (e.error?.code === -32601) { - throw e + throw e; } } - async createBundle (): Promise<[UserOperation[], StorageMap]> { - const entries = this.mempoolManager.getSortedForInclusion() - const bundle: UserOperation[] = [] + async createBundle(): Promise<[UserOperation[], StorageMap]> { + const entries = this.mempoolManager.getSortedForInclusion(); + const bundle: UserOperation[] = []; // paymaster deposit should be enough for all UserOps in the bundle. - const paymasterDeposit: { [paymaster: string]: BigNumber } = {} + const paymasterDeposit: { [paymaster: string]: BigNumber } = {}; // throttled paymasters and deployers are allowed only small UserOps per bundle. - const stakedEntityCount: { [addr: string]: number } = {} + const stakedEntityCount: { [addr: string]: number } = {}; // each sender is allowed only once per bundle - const senders = new Set() + const senders = new Set(); // all entities that are known to be valid senders in the mempool - const knownSenders = this.mempoolManager.getKnownSenders() + const knownSenders = this.mempoolManager.getKnownSenders(); - const storageMap: StorageMap = {} - let totalGas = BigNumber.from(0) - debug('got mempool of ', entries.length) + const storageMap: StorageMap = {}; + let totalGas = BigNumber.from(0); + debug('got mempool of ', entries.length); // eslint-disable-next-line no-labels - mainLoop: - for (const entry of entries) { - const paymaster = getAddr(entry.userOp.paymasterAndData) - const factory = getAddr(entry.userOp.initCode) - const paymasterStatus = this.reputationManager.getStatus(paymaster) - const deployerStatus = this.reputationManager.getStatus(factory) - if (paymasterStatus === ReputationStatus.BANNED || deployerStatus === ReputationStatus.BANNED) { - this.mempoolManager.removeUserOp(entry.userOp) - continue + mainLoop: for (const entry of entries) { + const paymaster = getAddr(entry.userOp.paymasterAndData); + const factory = getAddr(entry.userOp.initCode); + const paymasterStatus = this.reputationManager.getStatus(paymaster); + const deployerStatus = this.reputationManager.getStatus(factory); + if ( + paymasterStatus === ReputationStatus.BANNED || + deployerStatus === ReputationStatus.BANNED + ) { + this.mempoolManager.removeUserOp(entry.userOp); + continue; } // [SREP-030] - if (paymaster != null && (paymasterStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[paymaster] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) { - debug('skipping throttled paymaster', entry.userOp.sender, entry.userOp.nonce) - continue + if ( + paymaster != null && + (paymasterStatus === ReputationStatus.THROTTLED ?? + (stakedEntityCount[paymaster] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT) + ) { + debug( + 'skipping throttled paymaster', + entry.userOp.sender, + entry.userOp.nonce, + ); + continue; } // [SREP-030] - if (factory != null && (deployerStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[factory] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) { - debug('skipping throttled factory', entry.userOp.sender, entry.userOp.nonce) - continue + if ( + factory != null && + (deployerStatus === ReputationStatus.THROTTLED ?? + (stakedEntityCount[factory] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT) + ) { + debug( + 'skipping throttled factory', + entry.userOp.sender, + entry.userOp.nonce, + ); + continue; } if (senders.has(entry.userOp.sender)) { - debug('skipping already included sender', entry.userOp.sender, entry.userOp.nonce) + debug( + 'skipping already included sender', + entry.userOp.sender, + entry.userOp.nonce, + ); // allow only a single UserOp per sender per bundle - continue + continue; } - let validationResult: ValidateUserOpResult + let validationResult: ValidateUserOpResult; try { // re-validate UserOp. no need to check stake, since it cannot be reduced between first and 2nd validation - validationResult = await this.validationManager.validateUserOp(entry.userOp, entry.referencedContracts, false) + validationResult = await this.validationManager.validateUserOp( + entry.userOp, + entry.referencedContracts, + false, + ); } catch (e: any) { - debug('failed 2nd validation:', e.message) + debug('failed 2nd validation:', e.message); // failed validation. don't try anymore - this.mempoolManager.removeUserOp(entry.userOp) - continue + this.mempoolManager.removeUserOp(entry.userOp); + continue; } for (const storageAddress of Object.keys(validationResult.storageMap)) { @@ -204,72 +258,99 @@ export class BundleManager { storageAddress.toLowerCase() !== entry.userOp.sender.toLowerCase() && knownSenders.includes(storageAddress.toLowerCase()) ) { - console.debug(`UserOperation from ${entry.userOp.sender} sender accessed a storage of another known sender ${storageAddress}`) + console.debug( + `UserOperation from ${entry.userOp.sender} sender accessed a storage of another known sender ${storageAddress}`, + ); // eslint-disable-next-line no-labels - continue mainLoop + continue mainLoop; } } // todo: we take UserOp's callGasLimit, even though it will probably require less (but we don't // attempt to estimate it to check) // which means we could "cram" more UserOps into a bundle. - const userOpGasCost = BigNumber.from(validationResult.returnInfo.preOpGas).add(entry.userOp.callGasLimit) - const newTotalGas = totalGas.add(userOpGasCost) + const userOpGasCost = BigNumber.from( + validationResult.returnInfo.preOpGas, + ).add(entry.userOp.callGasLimit); + const newTotalGas = totalGas.add(userOpGasCost); if (newTotalGas.gt(this.maxBundleGas)) { - break + break; } if (paymaster != null) { if (paymasterDeposit[paymaster] == null) { - paymasterDeposit[paymaster] = await this.entryPoint.balanceOf(paymaster) + paymasterDeposit[paymaster] = await this.entryPoint.balanceOf( + paymaster, + ); } - if (paymasterDeposit[paymaster].lt(validationResult.returnInfo.prefund)) { + if ( + paymasterDeposit[paymaster].lt(validationResult.returnInfo.prefund) + ) { // not enough balance in paymaster to pay for all UserOps // (but it passed validation, so it can sponsor them separately - continue + continue; } - stakedEntityCount[paymaster] = (stakedEntityCount[paymaster] ?? 0) + 1 - paymasterDeposit[paymaster] = paymasterDeposit[paymaster].sub(validationResult.returnInfo.prefund) + stakedEntityCount[paymaster] = (stakedEntityCount[paymaster] ?? 0) + 1; + paymasterDeposit[paymaster] = paymasterDeposit[paymaster].sub( + validationResult.returnInfo.prefund, + ); } if (factory != null) { - stakedEntityCount[factory] = (stakedEntityCount[factory] ?? 0) + 1 + stakedEntityCount[factory] = (stakedEntityCount[factory] ?? 0) + 1; } // If sender's account already exist: replace with its storage root hash - if (this.mergeToAccountRootHash && this.conditionalRpc && entry.userOp.initCode.length <= 2) { - const { storageHash } = await this.provider.send('eth_getProof', [entry.userOp.sender, [], 'latest']) - storageMap[entry.userOp.sender.toLowerCase()] = storageHash + if ( + this.mergeToAccountRootHash && + this.conditionalRpc && + entry.userOp.initCode.length <= 2 + ) { + const { storageHash } = await this.provider.send('eth_getProof', [ + entry.userOp.sender, + [], + 'latest', + ]); + storageMap[entry.userOp.sender.toLowerCase()] = storageHash; } - mergeStorageMap(storageMap, validationResult.storageMap) + mergeStorageMap(storageMap, validationResult.storageMap); - senders.add(entry.userOp.sender) - bundle.push(entry.userOp) - totalGas = newTotalGas + senders.add(entry.userOp.sender); + bundle.push(entry.userOp); + totalGas = newTotalGas; } - return [bundle, storageMap] + return [bundle, storageMap]; } /** * determine who should receive the proceedings of the request. * if signer's balance is too low, send it to signer. otherwise, send to configured beneficiary. */ - async _selectBeneficiary (): Promise { - const currentBalance = await this.provider.getBalance(this.signer.getAddress()) - let beneficiary = this.beneficiary + async _selectBeneficiary(): Promise { + const currentBalance = await this.provider.getBalance( + this.signer.getAddress(), + ); + let { beneficiary } = this; // below min-balance redeem to the signer, to keep it active. if (currentBalance.lte(this.minSignerBalance)) { - beneficiary = await this.signer.getAddress() - console.log('low balance. using ', beneficiary, 'as beneficiary instead of ', this.beneficiary) + beneficiary = await this.signer.getAddress(); + console.log( + 'low balance. using ', + beneficiary, + 'as beneficiary instead of ', + this.beneficiary, + ); } - return beneficiary + return beneficiary; } // helper function to get hashes of all UserOps - async getUserOpHashes (userOps: UserOperation[]): Promise { - const { userOpHashes } = await runContractScript(this.entryPoint.provider, + async getUserOpHashes(userOps: UserOperation[]): Promise { + const { userOpHashes } = await runContractScript( + this.entryPoint.provider, new GetUserOpHashes__factory(), - [this.entryPoint.address, userOps]) + [this.entryPoint.address, userOps], + ); - return userOpHashes + return userOpHashes; } } diff --git a/src/bundler/modules/EventsManager.ts b/src/bundler/modules/EventsManager.ts index 22e1380..47c50ea 100644 --- a/src/bundler/modules/EventsManager.ts +++ b/src/bundler/modules/EventsManager.ts @@ -1,99 +1,118 @@ -import { AccountDeployedEvent, UserOperationEventEvent } from '@account-abstraction/contracts/dist/types/EntryPoint' -import { ReputationManager } from './ReputationManager' -import { EntryPoint } from '@account-abstraction/contracts' -import Debug from 'debug' -import { SignatureAggregatorChangedEvent } from '@account-abstraction/contracts/types/EntryPoint' -import { TypedEvent } from '@account-abstraction/contracts/dist/types/common' -import { MempoolManager } from './MempoolManager' +import type { EntryPoint } from '@account-abstraction/contracts'; +import type { TypedEvent } from '@account-abstraction/contracts/dist/types/common'; +import type { + AccountDeployedEvent, + UserOperationEventEvent, +} from '@account-abstraction/contracts/dist/types/EntryPoint'; +import type { SignatureAggregatorChangedEvent } from '@account-abstraction/contracts/types/EntryPoint'; +import Debug from 'debug'; -const debug = Debug('aa.events') +import type { MempoolManager } from './MempoolManager'; +import type { ReputationManager } from './ReputationManager'; + +const debug = Debug('aa.events'); /** * listen to events. trigger ReputationManager's Included */ export class EventsManager { - lastBlock?: number + lastBlock?: number; - constructor ( + constructor( readonly entryPoint: EntryPoint, readonly mempoolManager: MempoolManager, - readonly reputationManager: ReputationManager) { - } + readonly reputationManager: ReputationManager, + ) {} /** * automatically listen to all UserOperationEvent events */ - initEventListener (): void { - this.entryPoint.on(this.entryPoint.filters.UserOperationEvent(), (...args) => { - const ev = args.slice(-1)[0] - void this.handleEvent(ev as any) - }) + initEventListener(): void { + this.entryPoint.on( + this.entryPoint.filters.UserOperationEvent(), + (...args) => { + const ev = args.slice(-1)[0]; + this.handleEvent(ev as any); + }, + ); } /** * process all new events since last run */ - async handlePastEvents (): Promise { + async handlePastEvents(): Promise { if (this.lastBlock === undefined) { - this.lastBlock = Math.max(1, await this.entryPoint.provider.getBlockNumber() - 1000) + this.lastBlock = Math.max( + 1, + (await this.entryPoint.provider.getBlockNumber()) - 1000, + ); } - const events = await this.entryPoint.queryFilter({ address: this.entryPoint.address }, this.lastBlock) + const events = await this.entryPoint.queryFilter( + { address: this.entryPoint.address }, + this.lastBlock, + ); for (const ev of events) { - this.handleEvent(ev) + this.handleEvent(ev); } } - handleEvent (ev: UserOperationEventEvent | AccountDeployedEvent | SignatureAggregatorChangedEvent): void { + handleEvent( + ev: + | UserOperationEventEvent + | AccountDeployedEvent + | SignatureAggregatorChangedEvent, + ): void { switch (ev.event) { case 'UserOperationEvent': - this.handleUserOperationEvent(ev as any) - break + this.handleUserOperationEvent(ev as any); + break; case 'AccountDeployed': - this.handleAccountDeployedEvent(ev as any) - break + this.handleAccountDeployedEvent(ev as any); + break; case 'SignatureAggregatorForUserOperations': - this.handleAggregatorChangedEvent(ev as any) - break + this.handleAggregatorChangedEvent(ev as any); + break; } - this.lastBlock = ev.blockNumber + 1 + this.lastBlock = ev.blockNumber + 1; } - handleAggregatorChangedEvent (ev: SignatureAggregatorChangedEvent): void { - debug('handle ', ev.event, ev.args.aggregator) - this.eventAggregator = ev.args.aggregator - this.eventAggregatorTxHash = ev.transactionHash + handleAggregatorChangedEvent(ev: SignatureAggregatorChangedEvent): void { + debug('handle ', ev.event, ev.args.aggregator); + this.eventAggregator = ev.args.aggregator; + this.eventAggregatorTxHash = ev.transactionHash; } - eventAggregator: string | null = null - eventAggregatorTxHash: string | null = null + eventAggregator: string | null = null; + + eventAggregatorTxHash: string | null = null; // aggregator event is sent once per events bundle for all UserOperationEvents in this bundle. // it is not sent at all if the transaction is handleOps - getEventAggregator (ev: TypedEvent): string | null { + getEventAggregator(ev: TypedEvent): string | null { if (ev.transactionHash !== this.eventAggregatorTxHash) { - this.eventAggregator = null - this.eventAggregatorTxHash = ev.transactionHash + this.eventAggregator = null; + this.eventAggregatorTxHash = ev.transactionHash; } - return this.eventAggregator + return this.eventAggregator; } // AccountDeployed event is sent before each UserOperationEvent that deploys a contract. - handleAccountDeployedEvent (ev: AccountDeployedEvent): void { - this._includedAddress(ev.args.factory) + handleAccountDeployedEvent(ev: AccountDeployedEvent): void { + this._includedAddress(ev.args.factory); } - handleUserOperationEvent (ev: UserOperationEventEvent): void { - const hash = ev.args.userOpHash - this.mempoolManager.removeUserOp(hash) - this._includedAddress(ev.args.sender) - this._includedAddress(ev.args.paymaster) - this._includedAddress(this.getEventAggregator(ev)) + handleUserOperationEvent(ev: UserOperationEventEvent): void { + const hash = ev.args.userOpHash; + this.mempoolManager.removeUserOp(hash); + this._includedAddress(ev.args.sender); + this._includedAddress(ev.args.paymaster); + this._includedAddress(this.getEventAggregator(ev)); } - _includedAddress (data: string | null): void { + _includedAddress(data: string | null): void { if (data != null && data.length >= 42) { - const addr = data.slice(0, 42) - this.reputationManager.updateIncludedStatus(addr) + const addr = data.slice(0, 42); + this.reputationManager.updateIncludedStatus(addr); } } } diff --git a/src/bundler/modules/ExecutionManager.ts b/src/bundler/modules/ExecutionManager.ts index 3d1357e..d5d7389 100644 --- a/src/bundler/modules/ExecutionManager.ts +++ b/src/bundler/modules/ExecutionManager.ts @@ -1,96 +1,128 @@ -import Debug from 'debug' -import { Mutex } from 'async-mutex' -import { ValidationManager } from '../../validation-manager' -import { UserOperation } from '../../utils' -import { clearInterval } from 'timers' +import { Mutex } from 'async-mutex'; +import Debug from 'debug'; +import { clearInterval } from 'timers'; -import { BundleManager, SendBundleReturn } from './BundleManager' -import { MempoolManager } from './MempoolManager' -import { ReputationManager } from './ReputationManager' +import type { BundleManager, SendBundleReturn } from './BundleManager'; +import type { MempoolManager } from './MempoolManager'; +import type { ReputationManager } from './ReputationManager'; +import type { UserOperation } from '../../utils'; +import type { ValidationManager } from '../../validation-manager'; -const debug = Debug('aa.exec') +const debug = Debug('aa.exec'); /** * execute userOps manually or using background timer. * This is the top-level interface to send UserOperation */ export class ExecutionManager { - private reputationCron: any - private autoBundleInterval: any - private maxMempoolSize = 0 // default to auto-mining - private autoInterval = 0 - private readonly mutex = new Mutex() + private reputationCron: any; - constructor (private readonly reputationManager: ReputationManager, + private autoBundleInterval: any; + + private maxMempoolSize = 0; // default to auto-mining + + private autoInterval = 0; + + private readonly mutex = new Mutex(); + + constructor( + private readonly reputationManager: ReputationManager, private readonly mempoolManager: MempoolManager, private readonly bundleManager: BundleManager, - private readonly validationManager: ValidationManager - ) { - } + private readonly validationManager: ValidationManager, + ) {} /** * send a user operation through the bundler. - * @param userOp the UserOp to send. + * @param userOp - the UserOp to send. + * @param entryPointInput */ - async sendUserOperation (userOp: UserOperation, entryPointInput: string): Promise { + async sendUserOperation( + userOp: UserOperation, + entryPointInput: string, + ): Promise { await this.mutex.runExclusive(async () => { - debug('sendUserOperation') - this.validationManager.validateInputParameters(userOp, entryPointInput) - const validationResult = await this.validationManager.validateUserOp(userOp, undefined) - const userOpHash = await this.validationManager.entryPoint.getUserOpHash(userOp) - this.mempoolManager.addUserOp(userOp, + debug('sendUserOperation'); + this.validationManager.validateInputParameters(userOp, entryPointInput); + const validationResult = await this.validationManager.validateUserOp( + userOp, + undefined, + ); + const userOpHash = await this.validationManager.entryPoint.getUserOpHash( + userOp, + ); + this.mempoolManager.addUserOp( + userOp, userOpHash, validationResult.returnInfo.prefund, validationResult.referencedContracts, validationResult.senderInfo, validationResult.paymasterInfo, validationResult.factoryInfo, - validationResult.aggregatorInfo) - await this.attemptBundle(false) - }) + validationResult.aggregatorInfo, + ); + await this.attemptBundle(false); + }); } - setReputationCron (interval: number): void { - debug('set reputation interval to', interval) - clearInterval(this.reputationCron) + setReputationCron(interval: number): void { + debug('set reputation interval to', interval); + clearInterval(this.reputationCron); if (interval !== 0) { - this.reputationCron = setInterval(() => this.reputationManager.hourlyCron(), interval) + this.reputationCron = setInterval( + () => this.reputationManager.hourlyCron(), + interval, + ); } } /** * set automatic bundle creation - * @param autoBundleInterval autoBundleInterval to check. send bundle anyway after this time is elapsed. zero for manual mode - * @param maxMempoolSize maximum # of pending mempool entities. send immediately when there are that many entities in the mempool. + * @param autoBundleInterval - autoBundleInterval to check. send bundle anyway after this time is elapsed. zero for manual mode + * @param maxMempoolSize - maximum # of pending mempool entities. send immediately when there are that many entities in the mempool. * set to zero (or 1) to automatically send each UserOp. * (note: there is a chance that the sent bundle will contain less than this number, in case only some mempool entities can be sent. * e.g. throttled paymaster) */ - setAutoBundler (autoBundleInterval: number, maxMempoolSize: number): void { - debug('set auto-bundle autoBundleInterval=', autoBundleInterval, 'maxMempoolSize=', maxMempoolSize) - clearInterval(this.autoBundleInterval) - this.autoInterval = autoBundleInterval + setAutoBundler(autoBundleInterval: number, maxMempoolSize: number): void { + debug( + 'set auto-bundle autoBundleInterval=', + autoBundleInterval, + 'maxMempoolSize=', + maxMempoolSize, + ); + clearInterval(this.autoBundleInterval); + this.autoInterval = autoBundleInterval; if (autoBundleInterval !== 0) { this.autoBundleInterval = setInterval(() => { - void this.attemptBundle(true).catch(e => console.error('auto-bundle failed', e)) - }, autoBundleInterval * 1000) + void this.attemptBundle(true).catch((e) => + console.error('auto-bundle failed', e), + ); + }, autoBundleInterval * 1000); } - this.maxMempoolSize = maxMempoolSize + this.maxMempoolSize = maxMempoolSize; } /** * attempt to send a bundle now. * @param force */ - async attemptBundle (force = true): Promise { - debug('attemptBundle force=', force, 'count=', this.mempoolManager.count(), 'max=', this.maxMempoolSize) + async attemptBundle(force = true): Promise { + debug( + 'attemptBundle force=', + force, + 'count=', + this.mempoolManager.count(), + 'max=', + this.maxMempoolSize, + ); if (force || this.mempoolManager.count() >= this.maxMempoolSize) { - const ret = await this.bundleManager.sendNextBundle() + const ret = await this.bundleManager.sendNextBundle(); if (this.maxMempoolSize === 0) { // in "auto-bundling" mode (which implies auto-mining) also flush mempool from included UserOps - await this.bundleManager.handlePastEvents() + await this.bundleManager.handlePastEvents(); } - return ret + return ret; } } } diff --git a/src/bundler/modules/MempoolManager.ts b/src/bundler/modules/MempoolManager.ts index 762063b..e8308b5 100644 --- a/src/bundler/modules/MempoolManager.ts +++ b/src/bundler/modules/MempoolManager.ts @@ -1,74 +1,71 @@ -import { BigNumber, BigNumberish } from 'ethers' -import { +import Debug from 'debug'; +import type { BigNumberish } from 'ethers'; +import { BigNumber } from 'ethers'; + +import type { ReputationManager } from './ReputationManager'; +import type { ReferencedCodeHashes, - RpcError, StakeInfo, UserOperation, - ValidationErrors, - getAddr, - requireCond -} from '../../utils' -import { ReputationManager } from './ReputationManager' -import Debug from 'debug' - -const debug = Debug('aa.mempool') - -export interface MempoolEntry { - userOp: UserOperation - userOpHash: string - prefund: BigNumberish - referencedContracts: ReferencedCodeHashes +} from '../../utils'; +import { RpcError, ValidationErrors, getAddr, requireCond } from '../../utils'; + +const debug = Debug('aa.mempool'); + +export type MempoolEntry = { + userOp: UserOperation; + userOpHash: string; + prefund: BigNumberish; + referencedContracts: ReferencedCodeHashes; // aggregator, if one was found during simulation - aggregator?: string -} + aggregator?: string; +}; -type MempoolDump = UserOperation[] +type MempoolDump = UserOperation[]; -const MAX_MEMPOOL_USEROPS_PER_SENDER = 4 -const THROTTLED_ENTITY_MEMPOOL_COUNT = 4 +const MAX_MEMPOOL_USEROPS_PER_SENDER = 4; +const THROTTLED_ENTITY_MEMPOOL_COUNT = 4; export class MempoolManager { - private mempool: MempoolEntry[] = [] + private mempool: MempoolEntry[] = []; // count entities in mempool. - private _entryCount: { [addr: string]: number | undefined } = {} + private _entryCount: { [addr: string]: number | undefined } = {}; - entryCount (address: string): number | undefined { - return this._entryCount[address.toLowerCase()] + entryCount(address: string): number | undefined { + return this._entryCount[address.toLowerCase()]; } - incrementEntryCount (address?: string): void { - address = address?.toLowerCase() + incrementEntryCount(address?: string): void { + address = address?.toLowerCase(); if (address == null) { - return + return; } - this._entryCount[address] = (this._entryCount[address] ?? 0) + 1 + this._entryCount[address] = (this._entryCount[address] ?? 0) + 1; } - decrementEntryCount (address?: string): void { - address = address?.toLowerCase() + decrementEntryCount(address?: string): void { + address = address?.toLowerCase(); if (address == null || this._entryCount[address] == null) { - return + return; } - this._entryCount[address] = (this._entryCount[address] ?? 0) - 1 + this._entryCount[address] = (this._entryCount[address] ?? 0) - 1; if ((this._entryCount[address] ?? 0) <= 0) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this._entryCount[address] + delete this._entryCount[address]; } } - constructor ( - readonly reputationManager: ReputationManager) { - } + constructor(readonly reputationManager: ReputationManager) {} - count (): number { - return this.mempool.length + count(): number { + return this.mempool.length; } // add userOp into the mempool, after initial validation. // replace existing, if any (and if new gas is higher) // revets if unable to add UserOp to mempool (too many UserOps with this sender) - addUserOp ( + addUserOp( userOp: UserOperation, userOpHash: string, prefund: BigNumberish, @@ -76,179 +73,223 @@ export class MempoolManager { senderInfo: StakeInfo, paymasterInfo?: StakeInfo, factoryInfo?: StakeInfo, - aggregatorInfo?: StakeInfo + aggregatorInfo?: StakeInfo, ): void { const entry: MempoolEntry = { userOp, userOpHash, prefund, referencedContracts, - aggregator: aggregatorInfo?.addr - } - const index = this._findBySenderNonce(userOp.sender, userOp.nonce) + aggregator: aggregatorInfo?.addr, + }; + const index = this._findBySenderNonce(userOp.sender, userOp.nonce); if (index !== -1) { - const oldEntry = this.mempool[index] - this.checkReplaceUserOp(oldEntry, entry) - debug('replace userOp', userOp.sender, userOp.nonce) - this.mempool[index] = entry + const oldEntry = this.mempool[index]; + this.checkReplaceUserOp(oldEntry, entry); + debug('replace userOp', userOp.sender, userOp.nonce); + this.mempool[index] = entry; } else { - debug('add userOp', userOp.sender, userOp.nonce) - this.incrementEntryCount(userOp.sender) - const paymaster = getAddr(userOp.paymasterAndData) + debug('add userOp', userOp.sender, userOp.nonce); + this.incrementEntryCount(userOp.sender); + const paymaster = getAddr(userOp.paymasterAndData); if (paymaster != null) { - this.incrementEntryCount(paymaster) + this.incrementEntryCount(paymaster); } - const factory = getAddr(userOp.initCode) + const factory = getAddr(userOp.initCode); if (factory != null) { - this.incrementEntryCount(factory) + this.incrementEntryCount(factory); } - this.checkReputation(senderInfo, paymasterInfo, factoryInfo, aggregatorInfo) - this.checkMultipleRolesViolation(userOp) - this.mempool.push(entry) + this.checkReputation( + senderInfo, + paymasterInfo, + factoryInfo, + aggregatorInfo, + ); + this.checkMultipleRolesViolation(userOp); + this.mempool.push(entry); } - this.updateSeenStatus(aggregatorInfo?.addr, userOp, senderInfo) + this.updateSeenStatus(aggregatorInfo?.addr, userOp, senderInfo); } - private updateSeenStatus (aggregator: string | undefined, userOp: UserOperation, senderInfo: StakeInfo): void { + private updateSeenStatus( + aggregator: string | undefined, + userOp: UserOperation, + senderInfo: StakeInfo, + ): void { try { - this.reputationManager.checkStake('account', senderInfo) - this.reputationManager.updateSeenStatus(userOp.sender) + this.reputationManager.checkStake('account', senderInfo); + this.reputationManager.updateSeenStatus(userOp.sender); } catch (e: any) { - if (!(e instanceof RpcError)) throw e + if (!(e instanceof RpcError)) { + throw e; + } } - this.reputationManager.updateSeenStatus(aggregator) - this.reputationManager.updateSeenStatus(getAddr(userOp.paymasterAndData)) - this.reputationManager.updateSeenStatus(getAddr(userOp.initCode)) + this.reputationManager.updateSeenStatus(aggregator); + this.reputationManager.updateSeenStatus(getAddr(userOp.paymasterAndData)); + this.reputationManager.updateSeenStatus(getAddr(userOp.initCode)); } // TODO: de-duplicate code // TODO 2: use configuration parameters instead of hard-coded constants - private checkReputation ( + private checkReputation( senderInfo: StakeInfo, paymasterInfo?: StakeInfo, factoryInfo?: StakeInfo, - aggregatorInfo?: StakeInfo): void { - this.checkReputationStatus('account', senderInfo, MAX_MEMPOOL_USEROPS_PER_SENDER) + aggregatorInfo?: StakeInfo, + ): void { + this.checkReputationStatus( + 'account', + senderInfo, + MAX_MEMPOOL_USEROPS_PER_SENDER, + ); if (paymasterInfo != null) { - this.checkReputationStatus('paymaster', paymasterInfo) + this.checkReputationStatus('paymaster', paymasterInfo); } if (factoryInfo != null) { - this.checkReputationStatus('deployer', factoryInfo) + this.checkReputationStatus('deployer', factoryInfo); } if (aggregatorInfo != null) { - this.checkReputationStatus('aggregator', aggregatorInfo) + this.checkReputationStatus('aggregator', aggregatorInfo); } } - private checkMultipleRolesViolation (userOp: UserOperation): void { - const knownEntities = this.getKnownEntities() + private checkMultipleRolesViolation(userOp: UserOperation): void { + const knownEntities = this.getKnownEntities(); requireCond( !knownEntities.includes(userOp.sender.toLowerCase()), `The sender address "${userOp.sender}" is used as a different entity in another UserOperation currently in mempool`, - ValidationErrors.OpcodeValidation - ) + ValidationErrors.OpcodeValidation, + ); - const knownSenders = this.getKnownSenders() - const paymaster = getAddr(userOp.paymasterAndData)?.toLowerCase() - const factory = getAddr(userOp.initCode)?.toLowerCase() + const knownSenders = this.getKnownSenders(); + const paymaster = getAddr(userOp.paymasterAndData)?.toLowerCase(); + const factory = getAddr(userOp.initCode)?.toLowerCase(); - const isPaymasterSenderViolation = knownSenders.includes(paymaster?.toLowerCase() ?? '') - const isFactorySenderViolation = knownSenders.includes(factory?.toLowerCase() ?? '') + const isPaymasterSenderViolation = knownSenders.includes( + paymaster?.toLowerCase() ?? '', + ); + const isFactorySenderViolation = knownSenders.includes( + factory?.toLowerCase() ?? '', + ); requireCond( !isPaymasterSenderViolation, `A Paymaster at ${paymaster} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.`, - ValidationErrors.OpcodeValidation - ) + ValidationErrors.OpcodeValidation, + ); requireCond( !isFactorySenderViolation, `A Factory at ${factory} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.`, - ValidationErrors.OpcodeValidation - ) + ValidationErrors.OpcodeValidation, + ); } - private checkReputationStatus ( + private checkReputationStatus( title: 'account' | 'paymaster' | 'aggregator' | 'deployer', stakeInfo: StakeInfo, - maxTxMempoolAllowedOverride?: number + maxTxMempoolAllowedOverride?: number, ): void { - const maxTxMempoolAllowedEntity = maxTxMempoolAllowedOverride ?? - this.reputationManager.calculateMaxAllowedMempoolOpsUnstaked(stakeInfo.addr) - this.reputationManager.checkBanned(title, stakeInfo) - const entryCount = this.entryCount(stakeInfo.addr) ?? 0 + const maxTxMempoolAllowedEntity = + maxTxMempoolAllowedOverride ?? + this.reputationManager.calculateMaxAllowedMempoolOpsUnstaked( + stakeInfo.addr, + ); + this.reputationManager.checkBanned(title, stakeInfo); + const entryCount = this.entryCount(stakeInfo.addr) ?? 0; if (entryCount > THROTTLED_ENTITY_MEMPOOL_COUNT) { - this.reputationManager.checkThrottled(title, stakeInfo) + this.reputationManager.checkThrottled(title, stakeInfo); } if (entryCount > maxTxMempoolAllowedEntity) { - this.reputationManager.checkStake(title, stakeInfo) + this.reputationManager.checkStake(title, stakeInfo); } } - private checkReplaceUserOp (oldEntry: MempoolEntry, entry: MempoolEntry): void { - const oldMaxPriorityFeePerGas = BigNumber.from(oldEntry.userOp.maxPriorityFeePerGas).toNumber() - const newMaxPriorityFeePerGas = BigNumber.from(entry.userOp.maxPriorityFeePerGas).toNumber() - const oldMaxFeePerGas = BigNumber.from(oldEntry.userOp.maxFeePerGas).toNumber() - const newMaxFeePerGas = BigNumber.from(entry.userOp.maxFeePerGas).toNumber() + private checkReplaceUserOp( + oldEntry: MempoolEntry, + entry: MempoolEntry, + ): void { + const oldMaxPriorityFeePerGas = BigNumber.from( + oldEntry.userOp.maxPriorityFeePerGas, + ).toNumber(); + const newMaxPriorityFeePerGas = BigNumber.from( + entry.userOp.maxPriorityFeePerGas, + ).toNumber(); + const oldMaxFeePerGas = BigNumber.from( + oldEntry.userOp.maxFeePerGas, + ).toNumber(); + const newMaxFeePerGas = BigNumber.from( + entry.userOp.maxFeePerGas, + ).toNumber(); // the error is "invalid fields", even though it is detected only after validation - requireCond(newMaxPriorityFeePerGas >= oldMaxPriorityFeePerGas * 1.1, - `Replacement UserOperation must have higher maxPriorityFeePerGas (old=${oldMaxPriorityFeePerGas} new=${newMaxPriorityFeePerGas}) `, ValidationErrors.InvalidFields) - requireCond(newMaxFeePerGas >= oldMaxFeePerGas * 1.1, - `Replacement UserOperation must have higher maxFeePerGas (old=${oldMaxFeePerGas} new=${newMaxFeePerGas}) `, ValidationErrors.InvalidFields) + requireCond( + newMaxPriorityFeePerGas >= oldMaxPriorityFeePerGas * 1.1, + `Replacement UserOperation must have higher maxPriorityFeePerGas (old=${oldMaxPriorityFeePerGas} new=${newMaxPriorityFeePerGas}) `, + ValidationErrors.InvalidFields, + ); + requireCond( + newMaxFeePerGas >= oldMaxFeePerGas * 1.1, + `Replacement UserOperation must have higher maxFeePerGas (old=${oldMaxFeePerGas} new=${newMaxFeePerGas}) `, + ValidationErrors.InvalidFields, + ); } - getSortedForInclusion (): MempoolEntry[] { - const copy = Array.from(this.mempool) + getSortedForInclusion(): MempoolEntry[] { + const copy = Array.from(this.mempool); - function cost (op: UserOperation): number { + /** + * + * @param op + */ + function cost(op: UserOperation): number { // TODO: need to consult basefee and maxFeePerGas - return BigNumber.from(op.maxPriorityFeePerGas).toNumber() + return BigNumber.from(op.maxPriorityFeePerGas).toNumber(); } - copy.sort((a, b) => cost(a.userOp) - cost(b.userOp)) - return copy + copy.sort((a, b) => cost(a.userOp) - cost(b.userOp)); + return copy; } - _findBySenderNonce (sender: string, nonce: BigNumberish): number { + _findBySenderNonce(sender: string, nonce: BigNumberish): number { for (let i = 0; i < this.mempool.length; i++) { - const curOp = this.mempool[i].userOp + const curOp = this.mempool[i].userOp; if (curOp.sender === sender && curOp.nonce === nonce) { - return i + return i; } } - return -1 + return -1; } - _findByHash (hash: string): number { + _findByHash(hash: string): number { for (let i = 0; i < this.mempool.length; i++) { - const curOp = this.mempool[i] + const curOp = this.mempool[i]; if (curOp.userOpHash === hash) { - return i + return i; } } - return -1 + return -1; } /** * remove UserOp from mempool. either it is invalid, or was included in a block * @param userOpOrHash */ - removeUserOp (userOpOrHash: UserOperation | string): void { - let index: number + removeUserOp(userOpOrHash: UserOperation | string): void { + let index: number; if (typeof userOpOrHash === 'string') { - index = this._findByHash(userOpOrHash) + index = this._findByHash(userOpOrHash); } else { - index = this._findBySenderNonce(userOpOrHash.sender, userOpOrHash.nonce) + index = this._findBySenderNonce(userOpOrHash.sender, userOpOrHash.nonce); } if (index !== -1) { - const userOp = this.mempool[index].userOp - debug('removeUserOp', userOp.sender, userOp.nonce) - this.mempool.splice(index, 1) - this.decrementEntryCount(userOp.sender) - this.decrementEntryCount(getAddr(userOp.paymasterAndData)) - this.decrementEntryCount(getAddr(userOp.initCode)) + const { userOp } = this.mempool[index]; + debug('removeUserOp', userOp.sender, userOp.nonce); + this.mempool.splice(index, 1); + this.decrementEntryCount(userOp.sender); + this.decrementEntryCount(getAddr(userOp.paymasterAndData)); + this.decrementEntryCount(getAddr(userOp.initCode)); // TODO: store and remove aggregator entity count } } @@ -256,44 +297,46 @@ export class MempoolManager { /** * debug: dump mempool content */ - dump (): MempoolDump { - return this.mempool.map(entry => entry.userOp) + dump(): MempoolDump { + return this.mempool.map((entry) => entry.userOp); } /** * for debugging: clear current in-memory state */ - clearState (): void { - this.mempool = [] - this._entryCount = {} + clearState(): void { + this.mempool = []; + this._entryCount = {}; } /** * Returns all addresses that are currently known to be "senders" according to the current mempool. */ - getKnownSenders (): string[] { - return this.mempool.map(it => { - return it.userOp.sender.toLowerCase() - }) + getKnownSenders(): string[] { + return this.mempool.map((it) => { + return it.userOp.sender.toLowerCase(); + }); } /** * Returns all addresses that are currently known to be any kind of entity according to the current mempool. * Note that "sender" addresses are not returned by this function. Use {@link getKnownSenders} instead. */ - getKnownEntities (): string[] { - const res = [] - const userOps = this.mempool + getKnownEntities(): string[] { + const res = []; + const userOps = this.mempool; res.push( - ...userOps.map(it => { - return getAddr(it.userOp.paymasterAndData) - }) - ) + ...userOps.map((it) => { + return getAddr(it.userOp.paymasterAndData); + }), + ); res.push( - ...userOps.map(it => { - return getAddr(it.userOp.initCode) - }) - ) - return res.filter(it => it != null).map(it => (it as string).toLowerCase()) + ...userOps.map((it) => { + return getAddr(it.userOp.initCode); + }), + ); + return res + .filter((it) => it != null) + .map((it) => (it as string).toLowerCase()); } } diff --git a/src/bundler/modules/ReputationManager.ts b/src/bundler/modules/ReputationManager.ts index 5f4dd29..f71f186 100644 --- a/src/bundler/modules/ReputationManager.ts +++ b/src/bundler/modules/ReputationManager.ts @@ -1,117 +1,123 @@ -import Debug from 'debug' -import { BigNumber } from 'ethers' -import { Provider } from '@ethersproject/providers' +import type { Provider } from '@ethersproject/providers'; +import Debug from 'debug'; +import { BigNumber } from 'ethers'; -import { StakeInfo, ValidationErrors, requireCond, tostr } from '../../utils' -import { IStakeManager__factory } from '../../contract-types' +import { IStakeManager__factory } from '../../contract-types'; +import type { StakeInfo } from '../../utils'; +import { ValidationErrors, requireCond, tostr } from '../../utils'; -const debug = Debug('aa.rep') +const debug = Debug('aa.rep'); /** * throttled entities are allowed minimal number of entries per bundle. banned entities are allowed none */ export enum ReputationStatus { - OK, THROTTLED, BANNED + OK, + THROTTLED, + BANNED, } -export interface ReputationParams { - minInclusionDenominator: number - throttlingSlack: number - banSlack: number -} +export type ReputationParams = { + minInclusionDenominator: number; + throttlingSlack: number; + banSlack: number; +}; export const BundlerReputationParams: ReputationParams = { minInclusionDenominator: 10, throttlingSlack: 10, - banSlack: 50 -} + banSlack: 50, +}; export const NonBundlerReputationParams: ReputationParams = { minInclusionDenominator: 100, throttlingSlack: 10, - banSlack: 10 -} + banSlack: 10, +}; -interface ReputationEntry { - address: string - opsSeen: number - opsIncluded: number - status?: ReputationStatus -} +type ReputationEntry = { + address: string; + opsSeen: number; + opsIncluded: number; + status?: ReputationStatus; +}; -export type ReputationDump = ReputationEntry[] +export type ReputationDump = ReputationEntry[]; export class ReputationManager { - constructor ( + constructor( readonly provider: Provider, readonly params: ReputationParams, readonly minStake: BigNumber, - readonly minUnstakeDelay: number) { - } + readonly minUnstakeDelay: number, + ) {} + + private entries: { [address: string]: ReputationEntry } = {}; - private entries: { [address: string]: ReputationEntry } = {} // black-listed entities - always banned - readonly blackList = new Set() + readonly blackList = new Set(); // white-listed entities - always OK. - readonly whitelist = new Set() + readonly whitelist = new Set(); /** * debug: dump reputation map (with updated "status" for each entry) */ - dump (): ReputationDump { - Object.values(this.entries).forEach(entry => { entry.status = this.getStatus(entry.address) }) - return Object.values(this.entries) + dump(): ReputationDump { + Object.values(this.entries).forEach((entry) => { + entry.status = this.getStatus(entry.address); + }); + return Object.values(this.entries); } /** * exponential backoff of opsSeen and opsIncluded values */ - hourlyCron (): void { - Object.keys(this.entries).forEach(addr => { - const entry = this.entries[addr] - entry.opsSeen = Math.floor(entry.opsSeen * 23 / 24) - entry.opsIncluded = Math.floor(entry.opsSeen * 23 / 24) + hourlyCron(): void { + Object.keys(this.entries).forEach((addr) => { + const entry = this.entries[addr]; + entry.opsSeen = Math.floor((entry.opsSeen * 23) / 24); + entry.opsIncluded = Math.floor((entry.opsSeen * 23) / 24); if (entry.opsIncluded === 0 && entry.opsSeen === 0) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.entries[addr] + delete this.entries[addr]; } - }) + }); } - addWhitelist (...params: string[]): void { - params.forEach(item => this.whitelist.add(item)) + addWhitelist(...params: string[]): void { + params.forEach((item) => this.whitelist.add(item)); } - addBlacklist (...params: string[]): void { - params.forEach(item => this.blackList.add(item)) + addBlacklist(...params: string[]): void { + params.forEach((item) => this.blackList.add(item)); } - _getOrCreate (addr: string): ReputationEntry { - addr = addr.toLowerCase() - let entry = this.entries[addr] + _getOrCreate(addr: string): ReputationEntry { + addr = addr.toLowerCase(); + let entry = this.entries[addr]; if (entry == null) { this.entries[addr] = entry = { address: addr, opsSeen: 0, - opsIncluded: 0 - } + opsIncluded: 0, + }; } - return entry + return entry; } /** * address seen in the mempool triggered by the * @param addr */ - updateSeenStatus (addr?: string): void { + updateSeenStatus(addr?: string): void { if (addr == null) { - return + return; } - const entry = this._getOrCreate(addr) - entry.opsSeen++ - debug('after seen++', addr, entry) + const entry = this._getOrCreate(addr); + entry.opsSeen++; + debug('after seen++', addr, entry); } /** @@ -119,56 +125,66 @@ export class ReputationManager { * triggered by the EventsManager. * @param addr */ - updateIncludedStatus (addr: string): void { - const entry = this._getOrCreate(addr) - entry.opsIncluded++ - debug('after Included++', addr, entry) + updateIncludedStatus(addr: string): void { + const entry = this._getOrCreate(addr); + entry.opsIncluded++; + debug('after Included++', addr, entry); } - isWhitelisted (addr: string): boolean { - return this.whitelist.has(addr) + isWhitelisted(addr: string): boolean { + return this.whitelist.has(addr); } // https://github.com/eth-infinitism/account-abstraction/blob/develop/eip/EIPS/eip-4337.md#reputation-scoring-and-throttlingbanning-for-paymasters - getStatus (addr?: string): ReputationStatus { - addr = addr?.toLowerCase() + getStatus(addr?: string): ReputationStatus { + addr = addr?.toLowerCase(); if (addr == null || this.whitelist.has(addr)) { - return ReputationStatus.OK + return ReputationStatus.OK; } if (this.blackList.has(addr)) { - return ReputationStatus.BANNED + return ReputationStatus.BANNED; } - const entry = this.entries[addr] + const entry = this.entries[addr]; if (entry == null) { - return ReputationStatus.OK + return ReputationStatus.OK; } - const minExpectedIncluded = Math.floor(entry.opsSeen / this.params.minInclusionDenominator) - if (minExpectedIncluded <= entry.opsIncluded + this.params.throttlingSlack) { - return ReputationStatus.OK - } else if (minExpectedIncluded <= entry.opsIncluded + this.params.banSlack) { - return ReputationStatus.THROTTLED - } else { - return ReputationStatus.BANNED + const minExpectedIncluded = Math.floor( + entry.opsSeen / this.params.minInclusionDenominator, + ); + if ( + minExpectedIncluded <= + entry.opsIncluded + this.params.throttlingSlack + ) { + return ReputationStatus.OK; + } else if ( + minExpectedIncluded <= + entry.opsIncluded + this.params.banSlack + ) { + return ReputationStatus.THROTTLED; } + return ReputationStatus.BANNED; } - async getStakeStatus (address: string, entryPointAddress: string): Promise<{ - stakeInfo: StakeInfo - isStaked: boolean + async getStakeStatus( + address: string, + entryPointAddress: string, + ): Promise<{ + stakeInfo: StakeInfo; + isStaked: boolean; }> { - const sm = IStakeManager__factory.connect(entryPointAddress, this.provider) - const info = await sm.getDepositInfo(address) + const sm = IStakeManager__factory.connect(entryPointAddress, this.provider); + const info = await sm.getDepositInfo(address); const isStaked = BigNumber.from(info.stake).gte(this.minStake) && - BigNumber.from(info.unstakeDelaySec).gte(this.minUnstakeDelay) + BigNumber.from(info.unstakeDelaySec).gte(this.minUnstakeDelay); return { stakeInfo: { addr: address, stake: info.stake.toString(), - unstakeDelaySec: info.unstakeDelaySec.toString() + unstakeDelaySec: info.unstakeDelaySec.toString(), }, - isStaked - } + isStaked, + }; } /** @@ -176,99 +192,138 @@ export class ReputationManager { * should be banned immediately, by increasing its opSeen counter * @param addr */ - crashedHandleOps (addr: string | undefined): void { + crashedHandleOps(addr: string | undefined): void { if (addr == null) { - return + return; } // todo: what value to put? how long do we want this banning to hold? - const entry = this._getOrCreate(addr) + const entry = this._getOrCreate(addr); // [SREP-050] - entry.opsSeen += 10000 - entry.opsIncluded = 0 - debug('crashedHandleOps', addr, entry) + entry.opsSeen += 10000; + entry.opsIncluded = 0; + debug('crashedHandleOps', addr, entry); } /** * for debugging: clear in-memory state */ - clearState (): void { - this.entries = {} + clearState(): void { + this.entries = {}; } /** * for debugging: put in the given reputation entries * @param entries + * @param reputations */ - setReputation (reputations: ReputationDump): ReputationDump { - reputations.forEach(rep => { + setReputation(reputations: ReputationDump): ReputationDump { + reputations.forEach((rep) => { this.entries[rep.address.toLowerCase()] = { address: rep.address, opsSeen: rep.opsSeen, - opsIncluded: rep.opsIncluded - } - }) - return this.dump() + opsIncluded: rep.opsIncluded, + }; + }); + return this.dump(); } /** * check the given address (account/paymaster/deployer/aggregator) is banned * unlike {@link checkStake} does not check whitelist or stake + * @param title + * @param info */ - checkBanned (title: 'account' | 'paymaster' | 'aggregator' | 'deployer', info: StakeInfo): void { - requireCond(this.getStatus(info.addr) !== ReputationStatus.BANNED, + checkBanned( + title: 'account' | 'paymaster' | 'aggregator' | 'deployer', + info: StakeInfo, + ): void { + requireCond( + this.getStatus(info.addr) !== ReputationStatus.BANNED, `${title} ${info.addr} is banned`, - ValidationErrors.Reputation, { [title]: info.addr }) + ValidationErrors.Reputation, + { [title]: info.addr }, + ); } /** * check the given address (account/paymaster/deployer/aggregator) is throttled * unlike {@link checkStake} does not check whitelist or stake + * @param title + * @param info */ - checkThrottled (title: 'account' | 'paymaster' | 'aggregator' | 'deployer', info: StakeInfo): void { - requireCond(this.getStatus(info.addr) !== ReputationStatus.THROTTLED, + checkThrottled( + title: 'account' | 'paymaster' | 'aggregator' | 'deployer', + info: StakeInfo, + ): void { + requireCond( + this.getStatus(info.addr) !== ReputationStatus.THROTTLED, `${title} ${info.addr} is throttled`, - ValidationErrors.Reputation, { [title]: info.addr }) + ValidationErrors.Reputation, + { [title]: info.addr }, + ); } /** * check the given address (account/paymaster/deployer/aggregator) is staked - * @param title the address title (field name to put into the "data" element) - * @param raddr the address to check the stake of. null is "ok" - * @param info stake info from verification. if not given, then read from entryPoint + * @param title - the address title (field name to put into the "data" element) + * @param raddr - the address to check the stake of. null is "ok" + * @param info - stake info from verification. if not given, then read from entryPoint */ - checkStake (title: 'account' | 'paymaster' | 'aggregator' | 'deployer', info?: StakeInfo): void { + checkStake( + title: 'account' | 'paymaster' | 'aggregator' | 'deployer', + info?: StakeInfo, + ): void { if (info?.addr == null || this.isWhitelisted(info.addr)) { - return + return; } - requireCond(this.getStatus(info.addr) !== ReputationStatus.BANNED, + requireCond( + this.getStatus(info.addr) !== ReputationStatus.BANNED, `${title} ${info.addr} is banned`, - ValidationErrors.Reputation, { [title]: info.addr }) + ValidationErrors.Reputation, + { [title]: info.addr }, + ); - requireCond(BigNumber.from(info.stake).gte(this.minStake), - `${title} ${info.addr} ${tostr(info.stake) === '0' ? 'is unstaked' : `stake ${tostr(info.stake)} is too low (min=${tostr(this.minStake)})`}`, - ValidationErrors.InsufficientStake) - requireCond(BigNumber.from(info.unstakeDelaySec).gte(this.minUnstakeDelay), - `${title} ${info.addr} unstake delay ${tostr(info.unstakeDelaySec)} is too low (min=${tostr(this.minUnstakeDelay)})`, - ValidationErrors.InsufficientStake) + requireCond( + BigNumber.from(info.stake).gte(this.minStake), + `${title} ${info.addr} ${ + tostr(info.stake) === '0' + ? 'is unstaked' + : `stake ${tostr(info.stake)} is too low (min=${tostr( + this.minStake, + )})` + }`, + ValidationErrors.InsufficientStake, + ); + requireCond( + BigNumber.from(info.unstakeDelaySec).gte(this.minUnstakeDelay), + `${title} ${info.addr} unstake delay ${tostr( + info.unstakeDelaySec, + )} is too low (min=${tostr(this.minUnstakeDelay)})`, + ValidationErrors.InsufficientStake, + ); } /** * @param entity - the address of a non-sender unstaked entity. * @returns maxMempoolCount - the number of UserOperations this entity is allowed to have in the mempool. */ - calculateMaxAllowedMempoolOpsUnstaked (entity: string): number { - entity = entity.toLowerCase() - const SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT = 10 - const entry = this.entries[entity] + calculateMaxAllowedMempoolOpsUnstaked(entity: string): number { + entity = entity.toLowerCase(); + const SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT = 10; + const entry = this.entries[entity]; if (entry == null) { - return SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT + return SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT; } - const INCLUSION_RATE_FACTOR = 10 - let inclusionRate = entry.opsIncluded / entry.opsSeen + const INCLUSION_RATE_FACTOR = 10; + let inclusionRate = entry.opsIncluded / entry.opsSeen; if (entry.opsSeen === 0) { // prevent NaN of Infinity in tests - inclusionRate = 0 + inclusionRate = 0; } - return SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT + Math.floor(inclusionRate * INCLUSION_RATE_FACTOR) + (Math.min(entry.opsIncluded, 10000)) + return ( + SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT + + Math.floor(inclusionRate * INCLUSION_RATE_FACTOR) + + Math.min(entry.opsIncluded, 10000) + ); } } diff --git a/src/bundler/modules/initServer.ts b/src/bundler/modules/initServer.ts index e379a34..cedf67f 100644 --- a/src/bundler/modules/initServer.ts +++ b/src/bundler/modules/initServer.ts @@ -1,14 +1,18 @@ -import { ExecutionManager } from './ExecutionManager' -import { BundlerReputationParams, ReputationManager } from './ReputationManager' -import { MempoolManager } from './MempoolManager' -import { BundleManager } from './BundleManager' -import { ValidationManager } from '../../validation-manager' -import { EntryPoint__factory } from '@account-abstraction/contracts' -import { parseEther } from 'ethers/lib/utils' -import { Signer } from 'ethers' -import { BundlerConfig } from '../BundlerConfig' -import { EventsManager } from './EventsManager' -import { getNetworkProvider } from '../Config' +import { EntryPoint__factory } from '@account-abstraction/contracts'; +import type { Signer } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; + +import { BundleManager } from './BundleManager'; +import { EventsManager } from './EventsManager'; +import { ExecutionManager } from './ExecutionManager'; +import { MempoolManager } from './MempoolManager'; +import { + BundlerReputationParams, + ReputationManager, +} from './ReputationManager'; +import { ValidationManager } from '../../validation-manager'; +import type { BundlerConfig } from '../BundlerConfig'; +import { getNetworkProvider } from '../Config'; /** * initialize server modules. @@ -16,19 +20,48 @@ import { getNetworkProvider } from '../Config' * @param config * @param signer */ -export function initServer (config: BundlerConfig, signer: Signer): [ExecutionManager, EventsManager, ReputationManager, MempoolManager] { - const entryPoint = EntryPoint__factory.connect(config.entryPoint, signer) - const reputationManager = new ReputationManager(getNetworkProvider(config.network), BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - const mempoolManager = new MempoolManager(reputationManager) - const validationManager = new ValidationManager(entryPoint, config.unsafe) - const eventsManager = new EventsManager(entryPoint, mempoolManager, reputationManager) - const bundleManager = new BundleManager(entryPoint, eventsManager, mempoolManager, validationManager, reputationManager, - config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc) - const executionManager = new ExecutionManager(reputationManager, mempoolManager, bundleManager, validationManager) +export function initServer( + config: BundlerConfig, + signer: Signer, +): [ExecutionManager, EventsManager, ReputationManager, MempoolManager] { + const entryPoint = EntryPoint__factory.connect(config.entryPoint, signer); + const reputationManager = new ReputationManager( + getNetworkProvider(config.network), + BundlerReputationParams, + parseEther(config.minStake), + config.minUnstakeDelay, + ); + const mempoolManager = new MempoolManager(reputationManager); + const validationManager = new ValidationManager(entryPoint, config.unsafe); + const eventsManager = new EventsManager( + entryPoint, + mempoolManager, + reputationManager, + ); + const bundleManager = new BundleManager( + entryPoint, + eventsManager, + mempoolManager, + validationManager, + reputationManager, + config.beneficiary, + parseEther(config.minBalance), + config.maxBundleGas, + config.conditionalRpc, + ); + const executionManager = new ExecutionManager( + reputationManager, + mempoolManager, + bundleManager, + validationManager, + ); - reputationManager.addWhitelist(...config.whitelist ?? []) - reputationManager.addBlacklist(...config.blacklist ?? []) - executionManager.setAutoBundler(config.autoBundleInterval, config.autoBundleMempoolSize) + reputationManager.addWhitelist(...(config.whitelist ?? [])); + reputationManager.addBlacklist(...(config.blacklist ?? [])); + executionManager.setAutoBundler( + config.autoBundleInterval, + config.autoBundleMempoolSize, + ); - return [executionManager, eventsManager, reputationManager, mempoolManager] + return [executionManager, eventsManager, reputationManager, mempoolManager]; } diff --git a/src/bundler/runBundler.ts b/src/bundler/runBundler.ts index a49e454..378e953 100644 --- a/src/bundler/runBundler.ts +++ b/src/bundler/runBundler.ts @@ -1,40 +1,46 @@ -import fs from 'fs' - -import { Command } from 'commander' -import { erc4337RuntimeVersion, RpcError, supportsRpcMethod } from '../utils' -import { ethers, Wallet, Signer } from 'ethers' - -import { BundlerServer } from './BundlerServer' -import { UserOpMethodHandler } from './UserOpMethodHandler' -import { EntryPoint, EntryPoint__factory } from '@account-abstraction/contracts' - -import { initServer } from './modules/initServer' -import { DebugMethodHandler } from './DebugMethodHandler' -import { DeterministicDeployer } from '../sdk' -import { supportsDebugTraceCall } from '../validation-manager' -import { resolveConfiguration } from './Config' -import { bundlerConfigDefault } from './BundlerConfig' -import { JsonRpcProvider } from '@ethersproject/providers' -import { parseEther } from 'ethers/lib/utils' +import type { EntryPoint } from '@account-abstraction/contracts'; +import { EntryPoint__factory } from '@account-abstraction/contracts'; +import type { JsonRpcProvider } from '@ethersproject/providers'; +import { Command } from 'commander'; +import type { Signer } from 'ethers'; +import { ethers, Wallet } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; +import fs from 'fs'; + +import { bundlerConfigDefault } from './BundlerConfig'; +import { BundlerServer } from './BundlerServer'; +import { resolveConfiguration } from './Config'; +import { DebugMethodHandler } from './DebugMethodHandler'; +import { initServer } from './modules/initServer'; +import { UserOpMethodHandler } from './UserOpMethodHandler'; +import { DeterministicDeployer } from '../sdk'; +import { erc4337RuntimeVersion, RpcError, supportsRpcMethod } from '../utils'; +import { supportsDebugTraceCall } from '../validation-manager'; // this is done so that console.log outputs BigNumber as hex string instead of unreadable object -export const inspectCustomSymbol = Symbol.for('nodejs.util.inspect.custom') +export const inspectCustomSymbol = Symbol.for('nodejs.util.inspect.custom'); // @ts-ignore ethers.BigNumber.prototype[inspectCustomSymbol] = function () { - return `BigNumber ${parseInt(this._hex)}` -} + return `BigNumber ${parseInt(this._hex)}`; +}; -const CONFIG_FILE_NAME = 'workdir/bundler.config.json' +const CONFIG_FILE_NAME = 'workdir/bundler.config.json'; -export let showStackTraces = false +export let showStackTraces = false; -export async function connectContracts ( +/** + * + * @param wallet + * @param entryPointAddress + */ +export async function connectContracts( wallet: Signer, - entryPointAddress: string): Promise<{ entryPoint: EntryPoint }> { - const entryPoint = EntryPoint__factory.connect(entryPointAddress, wallet) + entryPointAddress: string, +): Promise<{ entryPoint: EntryPoint }> { + const entryPoint = EntryPoint__factory.connect(entryPointAddress, wallet); return { - entryPoint - } + entryPoint, + }; } /** @@ -43,134 +49,195 @@ export async function connectContracts ( * @param argv * @param overrideExit */ -export async function runBundler (argv: string[], overrideExit = true): Promise { - const program = new Command() +export async function runBundler( + argv: string[], + overrideExit = true, +): Promise { + const program = new Command(); if (overrideExit) { (program as any)._exit = (exitCode: any, code: any, message: any) => { class CommandError extends Error { - constructor (message: string, readonly code: any, readonly exitCode: any) { - super(message) + constructor( + message: string, + readonly code: any, + readonly exitCode: any, + ) { + super(message); } } - throw new CommandError(message, code, exitCode) - } + throw new CommandError(message, code, exitCode); + }; } program .version(erc4337RuntimeVersion) .option('--beneficiary ', 'address to receive funds') .option('--gasFactor ') - .option('--minBalance ', 'below this signer balance, keep fee for itself, ignoring "beneficiary" address ') + .option( + '--minBalance ', + 'below this signer balance, keep fee for itself, ignoring "beneficiary" address ', + ) .option('--network ', 'network name or url') .option('--mnemonic ', 'mnemonic/private-key file of signer account') - .option('--entryPoint ', 'address of the supported EntryPoint contract') - .option('--port ', `server listening port (default: ${bundlerConfigDefault.port})`) + .option( + '--entryPoint ', + 'address of the supported EntryPoint contract', + ) + .option( + '--port ', + `server listening port (default: ${bundlerConfigDefault.port})`, + ) .option('--config ', 'path to config file', CONFIG_FILE_NAME) - .option('--auto', 'automatic bundling (bypass config.autoBundleMempoolSize)', false) - .option('--unsafe', 'UNSAFE mode: no storage or opcode checks (safe mode requires geth)') - .option('--debugRpc', 'enable debug rpc methods (auto-enabled for test node') + .option( + '--auto', + 'automatic bundling (bypass config.autoBundleMempoolSize)', + false, + ) + .option( + '--unsafe', + 'UNSAFE mode: no storage or opcode checks (safe mode requires geth)', + ) + .option( + '--debugRpc', + 'enable debug rpc methods (auto-enabled for test node', + ) .option('--conditionalRpc', 'Use eth_sendRawTransactionConditional RPC)') .option('--show-stack-traces', 'Show stack traces.') - .option('--createMnemonic ', 'create the mnemonic file') + .option('--createMnemonic ', 'create the mnemonic file'); - const programOpts = program.parse(argv).opts() - showStackTraces = programOpts.showStackTraces + const programOpts = program.parse(argv).opts(); + showStackTraces = programOpts.showStackTraces; - console.log('command-line arguments: ', program.opts()) + console.log('command-line arguments: ', program.opts()); if (programOpts.createMnemonic != null) { - const mnemonicFile: string = programOpts.createMnemonic - console.log('Creating mnemonic in file', mnemonicFile) + const mnemonicFile: string = programOpts.createMnemonic; + console.log('Creating mnemonic in file', mnemonicFile); if (fs.existsSync(mnemonicFile)) { - throw new Error(`Can't --createMnemonic: out file ${mnemonicFile} already exists`) + throw new Error( + `Can't --createMnemonic: out file ${mnemonicFile} already exists`, + ); } - const newMnemonic = Wallet.createRandom().mnemonic.phrase - fs.writeFileSync(mnemonicFile, newMnemonic) - console.log('created mnemonic file', mnemonicFile) - process.exit(1) + const newMnemonic = Wallet.createRandom().mnemonic.phrase; + fs.writeFileSync(mnemonicFile, newMnemonic); + console.log('created mnemonic file', mnemonicFile); + process.exit(1); } - const { config, provider, wallet } = await resolveConfiguration(programOpts) + const { config, provider, wallet } = await resolveConfiguration(programOpts); const { // name: chainName, - chainId - } = await provider.getNetwork() + chainId, + } = await provider.getNetwork(); if (chainId === 31337) { if (config.debugRpc == null) { - console.log('== debugrpc was', config.debugRpc) - config.debugRpc = true + console.log('== debugrpc was', config.debugRpc); + config.debugRpc = true; } else { - console.log('== debugrpc already st', config.debugRpc) + console.log('== debugrpc already st', config.debugRpc); } - await new DeterministicDeployer(provider as any).deterministicDeploy(EntryPoint__factory.bytecode) + await new DeterministicDeployer(provider as any).deterministicDeploy( + EntryPoint__factory.bytecode, + ); if ((await wallet.getBalance()).eq(0)) { - console.log('=== testnet: fund signer') - const signer = (provider as JsonRpcProvider).getSigner() - await signer.sendTransaction({ to: await wallet.getAddress(), value: parseEther('1') }) + console.log('=== testnet: fund signer'); + const signer = (provider as JsonRpcProvider).getSigner(); + await signer.sendTransaction({ + to: await wallet.getAddress(), + value: parseEther('1'), + }); } } - if (config.conditionalRpc && !await supportsRpcMethod(provider as any, 'eth_sendRawTransactionConditional', [{}, {}])) { - console.error('FATAL: --conditionalRpc requires a node that support eth_sendRawTransactionConditional') - process.exit(1) + if ( + config.conditionalRpc && + !(await supportsRpcMethod( + provider as any, + 'eth_sendRawTransactionConditional', + [{}, {}], + )) + ) { + console.error( + 'FATAL: --conditionalRpc requires a node that support eth_sendRawTransactionConditional', + ); + process.exit(1); } - if (!config.unsafe && !await supportsDebugTraceCall(provider as any)) { - console.error('FATAL: full validation requires a node with debug_traceCall. for local UNSAFE mode: use --unsafe') - process.exit(1) + if (!config.unsafe && !(await supportsDebugTraceCall(provider as any))) { + console.error( + 'FATAL: full validation requires a node with debug_traceCall. for local UNSAFE mode: use --unsafe', + ); + process.exit(1); } - const { - entryPoint - } = await connectContracts(wallet, config.entryPoint) + const { entryPoint } = await connectContracts(wallet, config.entryPoint); // bundleSize=1 replicate current immediate bundling mode const execManagerConfig = { - ...config + ...config, // autoBundleMempoolSize: 0 - } + }; if (programOpts.auto === true) { - execManagerConfig.autoBundleMempoolSize = 0 - execManagerConfig.autoBundleInterval = 0 + execManagerConfig.autoBundleMempoolSize = 0; + execManagerConfig.autoBundleInterval = 0; } - const [execManager, eventsManager, reputationManager, mempoolManager] = initServer(execManagerConfig, entryPoint.signer) + const [execManager, eventsManager, reputationManager, mempoolManager] = + initServer(execManagerConfig, entryPoint.signer); const methodHandler = new UserOpMethodHandler( execManager, provider, wallet, config, - entryPoint - ) - eventsManager.initEventListener() - const debugHandler = config.debugRpc ?? false - ? new DebugMethodHandler(execManager, eventsManager, reputationManager, mempoolManager) - : new Proxy({}, { - get (target: {}, method: string, receiver: any): any { - throw new RpcError(`method debug_bundler_${method} is not supported`, -32601) - } - }) as DebugMethodHandler + entryPoint, + ); + eventsManager.initEventListener(); + const debugHandler = + config.debugRpc ?? false + ? new DebugMethodHandler( + execManager, + eventsManager, + reputationManager, + mempoolManager, + ) + : (new Proxy( + {}, + { + get(target: {}, method: string, receiver: any): any { + throw new RpcError( + `method debug_bundler_${method} is not supported`, + -32601, + ); + }, + }, + ) as DebugMethodHandler); const bundlerServer = new BundlerServer( methodHandler, debugHandler, config, provider, - wallet - ) + wallet, + ); void bundlerServer.asyncStart().then(async () => { - console.log('Bundle interval (seconds)', execManagerConfig.autoBundleInterval) - console.log('connected to network', await provider.getNetwork().then(net => { - return { - name: net.name, - chainId: net.chainId - } - })) - console.log(`running on http://localhost:${config.port}/rpc`) - }) - - return bundlerServer + console.log( + 'Bundle interval (seconds)', + execManagerConfig.autoBundleInterval, + ); + console.log( + 'connected to network', + await provider.getNetwork().then((net) => { + return { + name: net.name, + chainId: net.chainId, + }; + }), + ); + console.log(`running on http://localhost:${config.port}/rpc`); + }); + + return bundlerServer; } diff --git a/src/bundler/test/BundlerManager.test.ts b/src/bundler/test/BundlerManager.test.ts index fccdc5b..2d2a2ff 100644 --- a/src/bundler/test/BundlerManager.test.ts +++ b/src/bundler/test/BundlerManager.test.ts @@ -1,32 +1,40 @@ -import { EntryPoint, EntryPoint__factory } from '@account-abstraction/contracts' -import { parseEther } from 'ethers/lib/utils' -import { assert, expect } from 'chai' -import { BundlerReputationParams, ReputationManager } from '../modules/ReputationManager' -import { AddressZero, getUserOpHash, UserOperation } from '../../utils' - -import { ValidationManager, supportsDebugTraceCall } from '../../validation-manager' -import { DeterministicDeployer } from '../../sdk' -import { MempoolManager } from '../modules/MempoolManager' -import { BundleManager } from '../modules/BundleManager' -import { ethers } from 'hardhat' -import { BundlerConfig } from '../BundlerConfig' -import { TestFakeWalletToken__factory } from '../../contract-types' -import { UserOpMethodHandler } from '../UserOpMethodHandler' -import { ExecutionManager } from '../modules/ExecutionManager' -import { EventsManager } from '../modules/EventsManager' -import { createSigner } from './testUtils' +import type { EntryPoint } from '@account-abstraction/contracts'; +import { EntryPoint__factory } from '@account-abstraction/contracts'; +import { assert, expect } from 'chai'; +import { parseEther } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; + +import { createSigner } from './testUtils'; +import { TestFakeWalletToken__factory } from '../../contract-types'; +import { DeterministicDeployer } from '../../sdk'; +import type { UserOperation } from '../../utils'; +import { AddressZero, getUserOpHash } from '../../utils'; +import { + ValidationManager, + supportsDebugTraceCall, +} from '../../validation-manager'; +import type { BundlerConfig } from '../BundlerConfig'; +import { BundleManager } from '../modules/BundleManager'; +import { EventsManager } from '../modules/EventsManager'; +import { ExecutionManager } from '../modules/ExecutionManager'; +import { MempoolManager } from '../modules/MempoolManager'; +import { + BundlerReputationParams, + ReputationManager, +} from '../modules/ReputationManager'; +import { UserOpMethodHandler } from '../UserOpMethodHandler'; describe('#BundlerManager', () => { - let bm: BundleManager + let bm: BundleManager; - let entryPoint: EntryPoint + let entryPoint: EntryPoint; - const provider = ethers.provider - const signer = provider.getSigner() + const { provider } = ethers; + const signer = provider.getSigner(); before(async function () { - entryPoint = await new EntryPoint__factory(signer).deploy() - DeterministicDeployer.init(provider) + entryPoint = await new EntryPoint__factory(signer).deploy(); + DeterministicDeployer.init(provider); const config: BundlerConfig = { beneficiary: await signer.getAddress(), @@ -36,20 +44,33 @@ describe('#BundlerManager', () => { mnemonic: '', network: '', port: '3000', - unsafe: !await supportsDebugTraceCall(provider as any), + unsafe: !(await supportsDebugTraceCall(provider as any)), autoBundleInterval: 0, autoBundleMempoolSize: 0, maxBundleGas: 5e6, // minstake zero, since we don't fund deployer. minStake: '0', - minUnstakeDelay: 0 - } - - const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - const mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(entryPoint, repMgr, config.unsafe) - bm = new BundleManager(entryPoint, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas) - }) + minUnstakeDelay: 0, + }; + + const repMgr = new ReputationManager( + provider, + BundlerReputationParams, + parseEther(config.minStake), + config.minUnstakeDelay, + ); + const mempoolMgr = new MempoolManager(repMgr); + const validMgr = new ValidationManager(entryPoint, repMgr, config.unsafe); + bm = new BundleManager( + entryPoint, + mempoolMgr, + validMgr, + repMgr, + config.beneficiary, + parseEther(config.minBalance), + config.maxBundleGas, + ); + }); it('#getUserOpHashes', async () => { const userOp: UserOperation = { @@ -63,21 +84,21 @@ describe('#BundlerManager', () => { verificationGasLimit: 7, maxFeePerGas: 8, maxPriorityFeePerGas: 9, - preVerificationGas: 10 - } + preVerificationGas: 10, + }; - const hash = await entryPoint.getUserOpHash(userOp) - const bmHash = await bm.getUserOpHashes([userOp]) - expect(bmHash).to.eql([hash]) - }) + const hash = await entryPoint.getUserOpHash(userOp); + const bmHash = await bm.getUserOpHashes([userOp]); + expect(bmHash).to.eql([hash]); + }); describe('createBundle', function () { - let methodHandler: UserOpMethodHandler - let bundleMgr: BundleManager + let methodHandler: UserOpMethodHandler; + let bundleMgr: BundleManager; before(async function () { - const bundlerSigner = await createSigner() - const _entryPoint = entryPoint.connect(bundlerSigner) + const bundlerSigner = await createSigner(); + const _entryPoint = entryPoint.connect(bundlerSigner); const config: BundlerConfig = { beneficiary: await bundlerSigner.getAddress(), entryPoint: _entryPoint.address, @@ -86,46 +107,74 @@ describe('#BundlerManager', () => { mnemonic: '', network: '', port: '3000', - unsafe: !await supportsDebugTraceCall(provider as any), + unsafe: !(await supportsDebugTraceCall(provider as any)), conditionalRpc: false, autoBundleInterval: 0, autoBundleMempoolSize: 0, maxBundleGas: 5e6, // minstake zero, since we don't fund deployer. minStake: '0', - minUnstakeDelay: 0 - } - const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - const mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(_entryPoint, repMgr, config.unsafe) - const evMgr = new EventsManager(_entryPoint, mempoolMgr, repMgr) - bundleMgr = new BundleManager(_entryPoint, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) - const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr) - execManager.setAutoBundler(0, 1000) + minUnstakeDelay: 0, + }; + const repMgr = new ReputationManager( + provider, + BundlerReputationParams, + parseEther(config.minStake), + config.minUnstakeDelay, + ); + const mempoolMgr = new MempoolManager(repMgr); + const validMgr = new ValidationManager( + _entryPoint, + repMgr, + config.unsafe, + ); + const evMgr = new EventsManager(_entryPoint, mempoolMgr, repMgr); + bundleMgr = new BundleManager( + _entryPoint, + evMgr, + mempoolMgr, + validMgr, + repMgr, + config.beneficiary, + parseEther(config.minBalance), + config.maxBundleGas, + false, + ); + const execManager = new ExecutionManager( + repMgr, + mempoolMgr, + bundleMgr, + validMgr, + ); + execManager.setAutoBundler(0, 1000); methodHandler = new UserOpMethodHandler( execManager, provider, bundlerSigner, config, - _entryPoint - ) - }) + _entryPoint, + ); + }); it('should not include a UserOp that accesses the storage of a different known sender', async function () { - if (!await supportsDebugTraceCall(ethers.provider)) { - console.log('WARNING: opcode banning tests can only run with geth') - this.skip() + if (!(await supportsDebugTraceCall(ethers.provider))) { + console.log('WARNING: opcode banning tests can only run with geth'); + this.skip(); } - const wallet1 = await new TestFakeWalletToken__factory(signer).deploy(entryPoint.address) - const wallet2 = await new TestFakeWalletToken__factory(signer).deploy(entryPoint.address) + const wallet1 = await new TestFakeWalletToken__factory(signer).deploy( + entryPoint.address, + ); + const wallet2 = await new TestFakeWalletToken__factory(signer).deploy( + entryPoint.address, + ); - await wallet1.sudoSetBalance(wallet1.address, parseEther('1')) - await wallet1.sudoSetBalance(wallet2.address, parseEther('1')) - await wallet2.sudoSetAnotherWallet(wallet1.address) - const calldata1 = wallet2.address - const calldata2 = '0x' + await wallet1.sudoSetBalance(wallet1.address, parseEther('1')); + await wallet1.sudoSetBalance(wallet2.address, parseEther('1')); + await wallet2.sudoSetAnotherWallet(wallet1.address); + const calldata1 = wallet2.address; + const calldata2 = '0x'; const cEmptyUserOp: UserOperation = { sender: AddressZero, @@ -138,29 +187,32 @@ describe('#BundlerManager', () => { verificationGasLimit: '0x50000', maxFeePerGas: '0x0', maxPriorityFeePerGas: '0x0', - preVerificationGas: '0x50000' - } + preVerificationGas: '0x50000', + }; const userOp1: UserOperation = { ...cEmptyUserOp, sender: wallet1.address, - callData: calldata1 - } + callData: calldata1, + }; const userOp2: UserOperation = { ...cEmptyUserOp, sender: wallet2.address, - callData: calldata2 - } - await methodHandler.sendUserOperation(userOp1, entryPoint.address) - await methodHandler.sendUserOperation(userOp2, entryPoint.address) - - const bundle = await bundleMgr.sendNextBundle() - await bundleMgr.handlePastEvents() - const mempool = bundleMgr.mempoolManager.getSortedForInclusion() - - assert.equal(bundle!.userOpHashes.length, 1) - assert.equal(bundle!.userOpHashes[0], getUserOpHash(userOp1, entryPoint.address, await signer.getChainId())) - assert.equal(mempool.length, 1) - assert.equal(mempool[0].userOp.sender, wallet2.address) - }) - }) -}) + callData: calldata2, + }; + await methodHandler.sendUserOperation(userOp1, entryPoint.address); + await methodHandler.sendUserOperation(userOp2, entryPoint.address); + + const bundle = await bundleMgr.sendNextBundle(); + await bundleMgr.handlePastEvents(); + const mempool = bundleMgr.mempoolManager.getSortedForInclusion(); + + assert.equal(bundle!.userOpHashes.length, 1); + assert.equal( + bundle!.userOpHashes[0], + getUserOpHash(userOp1, entryPoint.address, await signer.getChainId()), + ); + assert.equal(mempool.length, 1); + assert.equal(mempool[0].userOp.sender, wallet2.address); + }); + }); +}); diff --git a/src/bundler/test/BundlerServer.test.ts b/src/bundler/test/BundlerServer.test.ts index f2f7cc6..128b08c 100644 --- a/src/bundler/test/BundlerServer.test.ts +++ b/src/bundler/test/BundlerServer.test.ts @@ -1,3 +1,3 @@ describe('BundleServer', function () { // it('preflightCheck') -}) +}); diff --git a/src/bundler/test/DebugMethodHandler.test.ts b/src/bundler/test/DebugMethodHandler.test.ts index 9d81499..aabb795 100644 --- a/src/bundler/test/DebugMethodHandler.test.ts +++ b/src/bundler/test/DebugMethodHandler.test.ts @@ -1,36 +1,49 @@ -import { DebugMethodHandler } from '../DebugMethodHandler' -import { ExecutionManager } from '../modules/ExecutionManager' -import { BundlerReputationParams, ReputationManager } from '../modules/ReputationManager' -import { BundlerConfig } from '../BundlerConfig' -import { parseEther } from 'ethers/lib/utils' -import { MempoolManager } from '../modules/MempoolManager' -import { ValidationManager, supportsDebugTraceCall } from '../../validation-manager' -import { BundleManager, SendBundleReturn } from '../modules/BundleManager' -import { UserOpMethodHandler } from '../UserOpMethodHandler' -import { ethers } from 'hardhat' -import { EntryPoint, EntryPoint__factory, SimpleAccountFactory__factory } from '@account-abstraction/contracts' -import { DeterministicDeployer, SimpleAccountAPI } from '../../sdk' -import { Signer, Wallet } from 'ethers' -import { resolveHexlify } from '../../utils' -import { expect } from 'chai' -import { createSigner } from './testUtils' -import { EventsManager } from '../modules/EventsManager' +import type { EntryPoint } from '@account-abstraction/contracts'; +import { + EntryPoint__factory, + SimpleAccountFactory__factory, +} from '@account-abstraction/contracts'; +import { expect } from 'chai'; +import type { Signer } from 'ethers'; +import { Wallet } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; -const provider = ethers.provider +import { createSigner } from './testUtils'; +import { DeterministicDeployer, SimpleAccountAPI } from '../../sdk'; +import { resolveHexlify } from '../../utils'; +import { + ValidationManager, + supportsDebugTraceCall, +} from '../../validation-manager'; +import type { BundlerConfig } from '../BundlerConfig'; +import { DebugMethodHandler } from '../DebugMethodHandler'; +import type { SendBundleReturn } from '../modules/BundleManager'; +import { BundleManager } from '../modules/BundleManager'; +import { EventsManager } from '../modules/EventsManager'; +import { ExecutionManager } from '../modules/ExecutionManager'; +import { MempoolManager } from '../modules/MempoolManager'; +import { + BundlerReputationParams, + ReputationManager, +} from '../modules/ReputationManager'; +import { UserOpMethodHandler } from '../UserOpMethodHandler'; + +const { provider } = ethers; describe('#DebugMethodHandler', () => { - let debugMethodHandler: DebugMethodHandler - let entryPoint: EntryPoint - let methodHandler: UserOpMethodHandler - let smartAccountAPI: SimpleAccountAPI - let signer: Signer - const accountSigner = Wallet.createRandom() + let debugMethodHandler: DebugMethodHandler; + let entryPoint: EntryPoint; + let methodHandler: UserOpMethodHandler; + let smartAccountAPI: SimpleAccountAPI; + let signer: Signer; + const accountSigner = Wallet.createRandom(); before(async () => { - signer = await createSigner() + signer = await createSigner(); - entryPoint = await new EntryPoint__factory(signer).deploy() - DeterministicDeployer.init(provider) + entryPoint = await new EntryPoint__factory(signer).deploy(); + DeterministicDeployer.init(provider); const config: BundlerConfig = { beneficiary: await signer.getAddress(), @@ -40,63 +53,92 @@ describe('#DebugMethodHandler', () => { mnemonic: '', network: '', port: '3000', - unsafe: !await supportsDebugTraceCall(provider as any), + unsafe: !(await supportsDebugTraceCall(provider as any)), conditionalRpc: false, autoBundleInterval: 0, autoBundleMempoolSize: 0, maxBundleGas: 5e6, // minstake zero, since we don't fund deployer. minStake: '0', - minUnstakeDelay: 0 - } + minUnstakeDelay: 0, + }; - const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - const mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(entryPoint, repMgr, config.unsafe) - const eventsManager = new EventsManager(entryPoint, mempoolMgr, repMgr) - const bundleMgr = new BundleManager(entryPoint, eventsManager, mempoolMgr, validMgr, repMgr, - config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) - const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr) + const repMgr = new ReputationManager( + provider, + BundlerReputationParams, + parseEther(config.minStake), + config.minUnstakeDelay, + ); + const mempoolMgr = new MempoolManager(repMgr); + const validMgr = new ValidationManager(entryPoint, repMgr, config.unsafe); + const eventsManager = new EventsManager(entryPoint, mempoolMgr, repMgr); + const bundleMgr = new BundleManager( + entryPoint, + eventsManager, + mempoolMgr, + validMgr, + repMgr, + config.beneficiary, + parseEther(config.minBalance), + config.maxBundleGas, + false, + ); + const execManager = new ExecutionManager( + repMgr, + mempoolMgr, + bundleMgr, + validMgr, + ); methodHandler = new UserOpMethodHandler( execManager, provider, signer, config, - entryPoint - ) + entryPoint, + ); - debugMethodHandler = new DebugMethodHandler(execManager, eventsManager, repMgr, mempoolMgr) + debugMethodHandler = new DebugMethodHandler( + execManager, + eventsManager, + repMgr, + mempoolMgr, + ); - DeterministicDeployer.init(ethers.provider) - const accountDeployerAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [entryPoint.address]) + DeterministicDeployer.init(ethers.provider); + const accountDeployerAddress = await DeterministicDeployer.deploy( + new SimpleAccountFactory__factory(), + 0, + [entryPoint.address], + ); smartAccountAPI = new SimpleAccountAPI({ provider, entryPointAddress: entryPoint.address, owner: accountSigner, - factoryAddress: accountDeployerAddress - }) - const accountAddress = await smartAccountAPI.getAccountAddress() + factoryAddress: accountDeployerAddress, + }); + const accountAddress = await smartAccountAPI.getAccountAddress(); await signer.sendTransaction({ to: accountAddress, - value: parseEther('1') - }) - }) + value: parseEther('1'), + }); + }); it('should return sendBundleNow hashes', async () => { - debugMethodHandler.setBundlingMode('manual') - const addr = await smartAccountAPI.getAccountAddress() + debugMethodHandler.setBundlingMode('manual'); + const addr = await smartAccountAPI.getAccountAddress(); const op1 = await smartAccountAPI.createSignedUserOp({ target: addr, - data: '0x' - }) - const userOpHash = await methodHandler.sendUserOperation(await resolveHexlify(op1), entryPoint.address) - const { - transactionHash, - userOpHashes - } = await debugMethodHandler.sendBundleNow() as SendBundleReturn - expect(userOpHashes).eql([userOpHash]) - const txRcpt = await provider.getTransactionReceipt(transactionHash) - expect(txRcpt.to).to.eq(entryPoint.address) - }) -}) + data: '0x', + }); + const userOpHash = await methodHandler.sendUserOperation( + await resolveHexlify(op1), + entryPoint.address, + ); + const { transactionHash, userOpHashes } = + (await debugMethodHandler.sendBundleNow()) as SendBundleReturn; + expect(userOpHashes).eql([userOpHash]); + const txRcpt = await provider.getTransactionReceipt(transactionHash); + expect(txRcpt.to).to.eq(entryPoint.address); + }); +}); diff --git a/src/bundler/test/UserOpMethodHandler.test.ts b/src/bundler/test/UserOpMethodHandler.test.ts index 5be1209..a7848a1 100644 --- a/src/bundler/test/UserOpMethodHandler.test.ts +++ b/src/bundler/test/UserOpMethodHandler.test.ts @@ -1,60 +1,71 @@ -import { BaseProvider } from '@ethersproject/providers' -import { assert, expect } from 'chai' -import { parseEther, resolveProperties } from 'ethers/lib/utils' - -import { BundlerConfig } from '../BundlerConfig' -import { +import type { EntryPoint, + UserOperationStruct, +} from '@account-abstraction/contracts'; +import { EntryPoint__factory, SimpleAccountFactory__factory, - UserOperationStruct -} from '@account-abstraction/contracts' - -import { Signer, Wallet } from 'ethers' -import { DeterministicDeployer, SimpleAccountAPI } from '../../sdk' -import { postExecutionDump } from '../../utils/postExecCheck' +} from '@account-abstraction/contracts'; +import type { UserOperationEventEvent } from '@account-abstraction/contracts/dist/types/EntryPoint'; +import type { BaseProvider } from '@ethersproject/providers'; +import { assert, expect } from 'chai'; +import type { Signer } from 'ethers'; +import { Wallet } from 'ethers'; +import { parseEther, resolveProperties } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; + +import { createSigner } from './testUtils'; +import type { SampleRecipient, TestRulesAccount } from '../../contract-types'; +import { TestRulesAccount__factory } from '../../contract-types'; +import { DeterministicDeployer, SimpleAccountAPI } from '../../sdk'; +import { resolveHexlify, waitFor } from '../../utils'; +import { postExecutionDump } from '../../utils/postExecCheck'; +import { + ValidationManager, + supportsDebugTraceCall, +} from '../../validation-manager'; +import type { BundlerConfig } from '../BundlerConfig'; +import { BundleManager } from '../modules/BundleManager'; +import { EventsManager } from '../modules/EventsManager'; +import { ExecutionManager } from '../modules/ExecutionManager'; +import { MempoolManager } from '../modules/MempoolManager'; import { - SampleRecipient, - TestRulesAccount, - TestRulesAccount__factory -} from '../../contract-types' -import { ValidationManager, supportsDebugTraceCall } from '../../validation-manager' -import { resolveHexlify, waitFor } from '../../utils' -import { UserOperationEventEvent } from '@account-abstraction/contracts/dist/types/EntryPoint' -import { UserOperationReceipt } from '../RpcTypes' -import { ExecutionManager } from '../modules/ExecutionManager' -import { BundlerReputationParams, ReputationManager } from '../modules/ReputationManager' -import { MempoolManager } from '../modules/MempoolManager' -import { BundleManager } from '../modules/BundleManager' -import { UserOpMethodHandler } from '../UserOpMethodHandler' -import { ethers } from 'hardhat' -import { createSigner } from './testUtils' -import { EventsManager } from '../modules/EventsManager' + BundlerReputationParams, + ReputationManager, +} from '../modules/ReputationManager'; +import type { UserOperationReceipt } from '../RpcTypes'; +import { UserOpMethodHandler } from '../UserOpMethodHandler'; describe('UserOpMethodHandler', function () { - const helloWorld = 'hello world' + const helloWorld = 'hello world'; - let accountDeployerAddress: string - let methodHandler: UserOpMethodHandler - let provider: BaseProvider - let signer: Signer - const accountSigner = Wallet.createRandom() - let mempoolMgr: MempoolManager + let accountDeployerAddress: string; + let methodHandler: UserOpMethodHandler; + let provider: BaseProvider; + let signer: Signer; + const accountSigner = Wallet.createRandom(); + let mempoolMgr: MempoolManager; - let entryPoint: EntryPoint - let sampleRecipient: SampleRecipient + let entryPoint: EntryPoint; + let sampleRecipient: SampleRecipient; before(async function () { - provider = ethers.provider + provider = ethers.provider; - signer = await createSigner() - entryPoint = await new EntryPoint__factory(signer).deploy() + signer = await createSigner(); + entryPoint = await new EntryPoint__factory(signer).deploy(); - DeterministicDeployer.init(ethers.provider) - accountDeployerAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [entryPoint.address]) + DeterministicDeployer.init(ethers.provider); + accountDeployerAddress = await DeterministicDeployer.deploy( + new SimpleAccountFactory__factory(), + 0, + [entryPoint.address], + ); - const sampleRecipientFactory = await ethers.getContractFactory('SampleRecipient') - sampleRecipient = await sampleRecipientFactory.deploy() + const sampleRecipientFactory = await ethers.getContractFactory( + 'SampleRecipient', + ); + sampleRecipient = await sampleRecipientFactory.deploy(); const config: BundlerConfig = { beneficiary: await signer.getAddress(), @@ -64,139 +75,199 @@ describe('UserOpMethodHandler', function () { mnemonic: '', network: '', port: '3000', - unsafe: !await supportsDebugTraceCall(provider as any), + unsafe: !(await supportsDebugTraceCall(provider as any)), conditionalRpc: false, autoBundleInterval: 0, autoBundleMempoolSize: 0, maxBundleGas: 5e6, // minstake zero, since we don't fund deployer. minStake: '0', - minUnstakeDelay: 0 - } + minUnstakeDelay: 0, + }; - const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) - mempoolMgr = new MempoolManager(repMgr) - const validMgr = new ValidationManager(entryPoint, repMgr, config.unsafe) - const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr) - const bundleMgr = new BundleManager(entryPoint, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) - const execManager = new ExecutionManager(repMgr, mempoolMgr, bundleMgr, validMgr) + const repMgr = new ReputationManager( + provider, + BundlerReputationParams, + parseEther(config.minStake), + config.minUnstakeDelay, + ); + mempoolMgr = new MempoolManager(repMgr); + const validMgr = new ValidationManager(entryPoint, repMgr, config.unsafe); + const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr); + const bundleMgr = new BundleManager( + entryPoint, + evMgr, + mempoolMgr, + validMgr, + repMgr, + config.beneficiary, + parseEther(config.minBalance), + config.maxBundleGas, + false, + ); + const execManager = new ExecutionManager( + repMgr, + mempoolMgr, + bundleMgr, + validMgr, + ); methodHandler = new UserOpMethodHandler( execManager, provider, signer, config, - entryPoint - ) - }) + entryPoint, + ); + }); describe('eth_supportedEntryPoints', function () { it('eth_supportedEntryPoints', async () => { - await expect(await methodHandler.getSupportedEntryPoints()).to.eql([entryPoint.address]) - }) - }) + await expect(await methodHandler.getSupportedEntryPoints()).to.eql([ + entryPoint.address, + ]); + }); + }); describe('query rpc calls: eth_estimateUserOperationGas, eth_callUserOperation', function () { - let owner: Wallet - let smartAccountAPI: SimpleAccountAPI - let target: string + let owner: Wallet; + let smartAccountAPI: SimpleAccountAPI; + let target: string; before('init', async () => { - owner = Wallet.createRandom() - target = await Wallet.createRandom().getAddress() + owner = Wallet.createRandom(); + target = await Wallet.createRandom().getAddress(); smartAccountAPI = new SimpleAccountAPI({ provider, entryPointAddress: entryPoint.address, owner, - factoryAddress: accountDeployerAddress - }) - }) + factoryAddress: accountDeployerAddress, + }); + }); it('estimateUserOperationGas should estimate even without eth', async () => { // fail without gas const op = await smartAccountAPI.createSignedUserOp({ target, - data: '0xdeadface' - }) - expect(await methodHandler.estimateUserOperationGas(await resolveHexlify(op), entryPoint.address).catch(e => e.message)).to.eql('AA21 didn\'t pay prefund') + data: '0xdeadface', + }); + expect( + await methodHandler + .estimateUserOperationGas( + await resolveHexlify(op), + entryPoint.address, + ) + .catch((e) => e.message), + ).to.eql("AA21 didn't pay prefund"); // should estimate with gasprice=0 const op1 = await smartAccountAPI.createSignedUserOp({ maxFeePerGas: 0, target, - data: '0xdeadface' - }) - const ret = await methodHandler.estimateUserOperationGas(await resolveHexlify(op1), entryPoint.address) + data: '0xdeadface', + }); + const ret = await methodHandler.estimateUserOperationGas( + await resolveHexlify(op1), + entryPoint.address, + ); // verification gas should be high - it creates this wallet - expect(ret.verificationGasLimit).to.be.closeTo(300000, 100000) + expect(ret.verificationGasLimit).to.be.closeTo(300000, 100000); // execution should be quite low. // (NOTE: actual execution should revert: it only succeeds because the wallet is NOT deployed yet, // and estimation doesn't perform full deploy-validate-execute cycle) - expect(ret.callGasLimit).to.be.closeTo(25000, 10000) - }) - }) + expect(ret.callGasLimit).to.be.closeTo(25000, 10000); + }); + }); describe('sendUserOperation', function () { - let userOperation: UserOperationStruct - let accountAddress: string + let userOperation: UserOperationStruct; + let accountAddress: string; - let accountDeployerAddress: string - let userOpHash: string + let accountDeployerAddress: string; + let userOpHash: string; before(async function () { - DeterministicDeployer.init(ethers.provider) - accountDeployerAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [entryPoint.address]) + DeterministicDeployer.init(ethers.provider); + accountDeployerAddress = await DeterministicDeployer.deploy( + new SimpleAccountFactory__factory(), + 0, + [entryPoint.address], + ); const smartAccountAPI = new SimpleAccountAPI({ provider, entryPointAddress: entryPoint.address, owner: accountSigner, - factoryAddress: accountDeployerAddress - }) - accountAddress = await smartAccountAPI.getAccountAddress() + factoryAddress: accountDeployerAddress, + }); + accountAddress = await smartAccountAPI.getAccountAddress(); await signer.sendTransaction({ to: accountAddress, - value: parseEther('1') - }) - - userOperation = await resolveProperties(await smartAccountAPI.createSignedUserOp({ - data: sampleRecipient.interface.encodeFunctionData('something', [helloWorld]), - target: sampleRecipient.address - })) - userOpHash = await methodHandler.sendUserOperation(await resolveHexlify(userOperation), entryPoint.address) - }) + value: parseEther('1'), + }); + + userOperation = await resolveProperties( + await smartAccountAPI.createSignedUserOp({ + data: sampleRecipient.interface.encodeFunctionData('something', [ + helloWorld, + ]), + target: sampleRecipient.address, + }), + ); + userOpHash = await methodHandler.sendUserOperation( + await resolveHexlify(userOperation), + entryPoint.address, + ); + }); it('should send UserOperation transaction to entryPoint', async function () { // sendUserOperation is async, even in auto-mining. need to wait for it. - const event = await waitFor(async () => await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(userOpHash)).then(ret => ret?.[0])) - - const transactionReceipt = await event!.getTransactionReceipt() - assert.isNotNull(transactionReceipt) - const logs = transactionReceipt.logs.filter(log => log.address === entryPoint.address) - .map(log => entryPoint.interface.parseLog(log)) - expect(logs.map(log => log.name)).to.eql([ + const event = await waitFor( + async () => + await entryPoint + .queryFilter(entryPoint.filters.UserOperationEvent(userOpHash)) + .then((ret) => ret?.[0]), + ); + + const transactionReceipt = await event!.getTransactionReceipt(); + assert.isNotNull(transactionReceipt); + const logs = transactionReceipt.logs + .filter((log) => log.address === entryPoint.address) + .map((log) => entryPoint.interface.parseLog(log)); + expect(logs.map((log) => log.name)).to.eql([ 'AccountDeployed', 'Deposited', 'BeforeExecution', - 'UserOperationEvent' - ]) - const [senderEvent] = await sampleRecipient.queryFilter(sampleRecipient.filters.Sender(), transactionReceipt.blockHash) - const userOperationEvent = logs[3] - - assert.equal(userOperationEvent.args.success, true) - - const expectedTxOrigin = await methodHandler.signer.getAddress() - assert.equal(senderEvent.args.txOrigin, expectedTxOrigin, 'sample origin should be bundler') - assert.equal(senderEvent.args.msgSender, accountAddress, 'sample msgsender should be account address') - }) + 'UserOperationEvent', + ]); + const [senderEvent] = await sampleRecipient.queryFilter( + sampleRecipient.filters.Sender(), + transactionReceipt.blockHash, + ); + const userOperationEvent = logs[3]; + + assert.equal(userOperationEvent.args.success, true); + + const expectedTxOrigin = await methodHandler.signer.getAddress(); + assert.equal( + senderEvent.args.txOrigin, + expectedTxOrigin, + 'sample origin should be bundler', + ); + assert.equal( + senderEvent.args.msgSender, + accountAddress, + 'sample msgsender should be account address', + ); + }); it('getUserOperationByHash should return submitted UserOp', async () => { - const ret = await methodHandler.getUserOperationByHash(userOpHash) - expect(ret?.entryPoint === entryPoint.address) - expect(ret?.userOperation.sender).to.eql(userOperation.sender) - expect(ret?.userOperation.callData).to.eql(userOperation.callData) - }) + const ret = await methodHandler.getUserOperationByHash(userOpHash); + expect(ret?.entryPoint === entryPoint.address); + expect(ret?.userOperation.sender).to.eql(userOperation.sender); + expect(ret?.userOperation.callData).to.eql(userOperation.callData); + }); it('getUserOperationReceipt should return receipt', async () => { - const rcpt = await methodHandler.getUserOperationReceipt(userOpHash) - expect(rcpt?.sender === userOperation.sender) - expect(rcpt?.success).to.be.true - }) + const rcpt = await methodHandler.getUserOperationReceipt(userOpHash); + expect(rcpt?.sender === userOperation.sender); + expect(rcpt?.success).to.be.true; + }); it('should expose FailedOp errors as text messages', async () => { const smartAccountAPI = new SimpleAccountAPI({ @@ -204,20 +275,25 @@ describe('UserOpMethodHandler', function () { entryPointAddress: entryPoint.address, owner: accountSigner, factoryAddress: accountDeployerAddress, - index: 1 - }) + index: 1, + }); const op = await smartAccountAPI.createSignedUserOp({ - data: sampleRecipient.interface.encodeFunctionData('something', [helloWorld]), - target: sampleRecipient.address - }) + data: sampleRecipient.interface.encodeFunctionData('something', [ + helloWorld, + ]), + target: sampleRecipient.address, + }); try { - await methodHandler.sendUserOperation(await resolveHexlify(op), entryPoint.address) - throw Error('expected fail') + await methodHandler.sendUserOperation( + await resolveHexlify(op), + entryPoint.address, + ); + throw Error('expected fail'); } catch (e: any) { - expect(e.message).to.match(/AA21 didn't pay prefund/) + expect(e.message).to.match(/AA21 didn't pay prefund/); } - }) + }); describe('validate get paid enough', function () { it('should pay just enough', async () => { @@ -225,80 +301,102 @@ describe('UserOpMethodHandler', function () { provider, entryPointAddress: entryPoint.address, accountAddress, - owner: accountSigner - }) + owner: accountSigner, + }); const op = await api.createSignedUserOp({ - data: sampleRecipient.interface.encodeFunctionData('something', [helloWorld]), + data: sampleRecipient.interface.encodeFunctionData('something', [ + helloWorld, + ]), target: sampleRecipient.address, - gasLimit: 1e6 - }) - const id = await methodHandler.sendUserOperation(await resolveHexlify(op), entryPoint.address) - - await postExecutionDump(entryPoint, id) - }) - it('should reject if doesn\'t pay enough', async () => { + gasLimit: 1e6, + }); + const id = await methodHandler.sendUserOperation( + await resolveHexlify(op), + entryPoint.address, + ); + + await postExecutionDump(entryPoint, id); + }); + it("should reject if doesn't pay enough", async () => { const api = new SimpleAccountAPI({ provider, entryPointAddress: entryPoint.address, accountAddress, owner: accountSigner, - overheads: { perUserOp: 0 } - }) + overheads: { perUserOp: 0 }, + }); const op = await api.createSignedUserOp({ - data: sampleRecipient.interface.encodeFunctionData('something', [helloWorld]), - target: sampleRecipient.address - }) + data: sampleRecipient.interface.encodeFunctionData('something', [ + helloWorld, + ]), + target: sampleRecipient.address, + }); try { - await methodHandler.sendUserOperation(await resolveHexlify(op), entryPoint.address) - throw new Error('expected to revert') + await methodHandler.sendUserOperation( + await resolveHexlify(op), + entryPoint.address, + ); + throw new Error('expected to revert'); } catch (e: any) { - expect(e.message).to.match(/preVerificationGas too low/) + expect(e.message).to.match(/preVerificationGas too low/); } - }) - }) - }) + }); + }); + }); describe('#_filterLogs', function () { // test events, good enough for _filterLogs - function userOpEv (hash: any): any { + /** + * + * @param hash + */ + function userOpEv(hash: any): any { return { - topics: ['userOpTopic', hash] - } as any + topics: ['userOpTopic', hash], + } as any; } - function ev (topic: any): UserOperationEventEvent { + /** + * + * @param topic + */ + function ev(topic: any): UserOperationEventEvent { return { - topics: [topic] - } as any + topics: [topic], + } as any; } - const ev1 = ev(1) - const ev2 = ev(2) - const ev3 = ev(3) - const u1 = userOpEv(10) - const u2 = userOpEv(20) - const u3 = userOpEv(30) + const ev1 = ev(1); + const ev2 = ev(2); + const ev3 = ev(3); + const u1 = userOpEv(10); + const u2 = userOpEv(20); + const u3 = userOpEv(30); it('should fail if no UserOperationEvent', async () => { - expect(() => methodHandler._filterLogs(u1, [ev1])).to.throw('no UserOperationEvent in logs') - }) + expect(() => methodHandler._filterLogs(u1, [ev1])).to.throw( + 'no UserOperationEvent in logs', + ); + }); it('should return empty array for single-op bundle with no events', async () => { - expect(methodHandler._filterLogs(u1, [u1])).to.eql([]) - }) + expect(methodHandler._filterLogs(u1, [u1])).to.eql([]); + }); it('should return events for single-op bundle', async () => { - expect(methodHandler._filterLogs(u1, [ev1, ev2, u1])).to.eql([ev1, ev2]) - }) + expect(methodHandler._filterLogs(u1, [ev1, ev2, u1])).to.eql([ev1, ev2]); + }); it('should return events for middle userOp in a bundle', async () => { - expect(methodHandler._filterLogs(u1, [ev2, u2, ev1, u1, ev3, u3])).to.eql([ev1]) - }) - }) + expect(methodHandler._filterLogs(u1, [ev2, u2, ev1, u1, ev3, u3])).to.eql( + [ev1], + ); + }); + }); describe('#getUserOperationReceipt', function () { - let userOpHash: string - let receipt: UserOperationReceipt - let acc: TestRulesAccount + let userOpHash: string; + let receipt: UserOperationReceipt; + let acc: TestRulesAccount; before(async () => { - acc = await new TestRulesAccount__factory(signer).deploy() - const callData = acc.interface.encodeFunctionData('execSendMessage') + acc = await new TestRulesAccount__factory(signer).deploy(); + const callData = acc.interface.encodeFunctionData('execSendMessage'); const op: UserOperationStruct = { sender: acc.address, @@ -311,52 +409,61 @@ describe('UserOpMethodHandler', function () { maxFeePerGas: 1e6, maxPriorityFeePerGas: 1e6, paymasterAndData: '0x', - signature: Buffer.from('emit-msg') - } - await entryPoint.depositTo(acc.address, { value: parseEther('1') }) + signature: Buffer.from('emit-msg'), + }; + await entryPoint.depositTo(acc.address, { value: parseEther('1') }); // await signer.sendTransaction({to:acc.address, value: parseEther('1')}) - userOpHash = await entryPoint.getUserOpHash(op) - const beneficiary = signer.getAddress() - await entryPoint.handleOps([op], beneficiary).then(async ret => await ret.wait()) - const rcpt = await methodHandler.getUserOperationReceipt(userOpHash) + userOpHash = await entryPoint.getUserOpHash(op); + const beneficiary = signer.getAddress(); + await entryPoint + .handleOps([op], beneficiary) + .then(async (ret) => await ret.wait()); + const rcpt = await methodHandler.getUserOperationReceipt(userOpHash); if (rcpt == null) { - throw new Error('getUserOperationReceipt returns null') + throw new Error('getUserOperationReceipt returns null'); } - receipt = rcpt - }) + receipt = rcpt; + }); it('should return null for nonexistent hash', async () => { - expect(await methodHandler.getUserOperationReceipt(ethers.constants.HashZero)).to.equal(null) - }) + expect( + await methodHandler.getUserOperationReceipt(ethers.constants.HashZero), + ).to.equal(null); + }); it('receipt should contain only userOp execution events..', async () => { - expect(receipt.logs.length).to.equal(1) - acc.interface.decodeEventLog('TestMessage', receipt.logs[0].data, receipt.logs[0].topics) - }) + expect(receipt.logs.length).to.equal(1); + acc.interface.decodeEventLog( + 'TestMessage', + receipt.logs[0].data, + receipt.logs[0].topics, + ); + }); it('general receipt fields', () => { - expect(receipt.success).to.equal(true) - expect(receipt.sender).to.equal(acc.address) - }) + expect(receipt.success).to.equal(true); + expect(receipt.sender).to.equal(acc.address); + }); it('receipt should carry transaction receipt', () => { // filter out BOR-specific events.. - const logs = receipt.receipt.logs - .filter(log => log.address !== '0x0000000000000000000000000000000000001010') + const logs = receipt.receipt.logs.filter( + (log) => log.address !== '0x0000000000000000000000000000000000001010', + ); const eventNames = logs // .filter(l => l.address == entryPoint.address) - .map(l => { + .map((l) => { try { - return entryPoint.interface.parseLog(l) + return entryPoint.interface.parseLog(l); } catch (e) { - return acc.interface.parseLog(l) + return acc.interface.parseLog(l); } }) - .map(l => l.name) + .map((l) => l.name); expect(eventNames).to.eql([ 'TestFromValidation', // account validateUserOp 'BeforeExecution', // entryPoint marker 'TestMessage', // account execution event - 'UserOperationEvent' // post-execution event - ]) - }) - }) -}) + 'UserOperationEvent', // post-execution event + ]); + }); + }); +}); diff --git a/src/bundler/test/ValidateManager.test.ts b/src/bundler/test/ValidateManager.test.ts index 3f25c02..fbfe825 100644 --- a/src/bundler/test/ValidateManager.test.ts +++ b/src/bundler/test/ValidateManager.test.ts @@ -1,35 +1,44 @@ -import { EntryPoint, EntryPoint__factory } from '@account-abstraction/contracts' -import { assert, expect } from 'chai' -import { defaultAbiCoder, hexConcat, hexlify, keccak256, parseEther } from 'ethers/lib/utils' -import { ethers } from 'hardhat' - -import { AddressZero, decodeErrorReason, toBytes32, UserOperation } from '../../utils' -import { - ValidateUserOpResult, - ValidationManager, - checkRulesViolations, - supportsDebugTraceCall -} from '../../validation-manager' - +import type { EntryPoint } from '@account-abstraction/contracts'; +import { EntryPoint__factory } from '@account-abstraction/contracts'; +import { assert, expect } from 'chai'; import { + defaultAbiCoder, + hexConcat, + hexlify, + keccak256, + parseEther, +} from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; + +import type { TestCoin, - TestCoin__factory, TestOpcodesAccount, TestOpcodesAccountFactory, + TestRulesAccount, + TestStorageAccount, + TestStorageAccountFactory, + TestTimeRangeAccountFactory, +} from '../../contract-types'; +import { + TestCoin__factory, TestOpcodesAccountFactory__factory, TestOpcodesAccount__factory, TestRecursionAccount__factory, - TestRulesAccount, TestRulesAccountFactory__factory, TestRulesAccount__factory, - TestStorageAccount, - TestStorageAccountFactory, TestStorageAccountFactory__factory, TestStorageAccount__factory, - TestTimeRangeAccountFactory, - TestTimeRangeAccountFactory__factory -} from '../../contract-types' -import { ReputationManager } from '../modules/ReputationManager' + TestTimeRangeAccountFactory__factory, +} from '../../contract-types'; +import type { UserOperation } from '../../utils'; +import { AddressZero, decodeErrorReason, toBytes32 } from '../../utils'; +import type { ValidateUserOpResult } from '../../validation-manager'; +import { + ValidationManager, + checkRulesViolations, + supportsDebugTraceCall, +} from '../../validation-manager'; +import { ReputationManager } from '../modules/ReputationManager'; const cEmptyUserOp: UserOperation = { sender: AddressZero, @@ -42,33 +51,69 @@ const cEmptyUserOp: UserOperation = { verificationGasLimit: 50000, maxFeePerGas: 0, maxPriorityFeePerGas: 0, - preVerificationGas: 0 -} + preVerificationGas: 0, +}; describe('#ValidationManager', () => { - let vm: ValidationManager - let opcodeFactory: TestOpcodesAccountFactory - let storageFactory: TestStorageAccountFactory - let testcoin: TestCoin - - let paymaster: TestOpcodesAccount - let entryPoint: EntryPoint - let rulesAccount: TestRulesAccount - let storageAccount: TestStorageAccount - - async function testUserOp (validateRule: string = '', pmRule?: string, initFunc?: string, factoryAddress = opcodeFactory.address): Promise { - const userOp = await createTestUserOp(validateRule, pmRule, initFunc, factoryAddress) - return { userOp, ...await vm.validateUserOp(userOp) } + let vm: ValidationManager; + let opcodeFactory: TestOpcodesAccountFactory; + let storageFactory: TestStorageAccountFactory; + let testcoin: TestCoin; + + let paymaster: TestOpcodesAccount; + let entryPoint: EntryPoint; + let rulesAccount: TestRulesAccount; + let storageAccount: TestStorageAccount; + + /** + * + * @param validateRule + * @param pmRule + * @param initFunc + * @param factoryAddress + */ + async function testUserOp( + validateRule = '', + pmRule?: string, + initFunc?: string, + factoryAddress = opcodeFactory.address, + ): Promise { + const userOp = await createTestUserOp( + validateRule, + pmRule, + initFunc, + factoryAddress, + ); + return { userOp, ...(await vm.validateUserOp(userOp)) }; } - async function testExistingUserOp (validateRule: string = '', pmRule = ''): Promise { - const userOp = await existingStorageAccountUserOp(validateRule, pmRule) - return { userOp, ...await vm.validateUserOp(userOp) } + /** + * + * @param validateRule + * @param pmRule + */ + async function testExistingUserOp( + validateRule = '', + pmRule = '', + ): Promise { + const userOp = await existingStorageAccountUserOp(validateRule, pmRule); + return { userOp, ...(await vm.validateUserOp(userOp)) }; } - async function existingStorageAccountUserOp (validateRule = '', pmRule = ''): Promise { - const paymasterAndData = pmRule === '' ? '0x' : hexConcat([paymaster.address, Buffer.from(pmRule)]) - const signature = hexlify(Buffer.from(validateRule)) + /** + * + * @param validateRule + * @param pmRule + */ + async function existingStorageAccountUserOp( + validateRule = '', + pmRule = '', + ): Promise { + const paymasterAndData = + pmRule === '' + ? '0x' + : hexConcat([paymaster.address, Buffer.from(pmRule)]); + const signature = hexlify(Buffer.from(validateRule)); return { ...cEmptyUserOp, sender: storageAccount.address, @@ -76,30 +121,42 @@ describe('#ValidationManager', () => { paymasterAndData, callGasLimit: 1e6, verificationGasLimit: 1e6, - preVerificationGas: 50000 - } + preVerificationGas: 50000, + }; } - async function createTestUserOp (validateRule: string = '', pmRule?: string, initFunc?: string, factoryAddress = opcodeFactory.address): Promise { + /** + * + * @param validateRule + * @param pmRule + * @param initFunc + * @param factoryAddress + */ + async function createTestUserOp( + validateRule = '', + pmRule?: string, + initFunc?: string, + factoryAddress = opcodeFactory.address, + ): Promise { if (initFunc === undefined) { - initFunc = opcodeFactory.interface.encodeFunctionData('create', ['']) + initFunc = opcodeFactory.interface.encodeFunctionData('create', ['']); } - const initCode = hexConcat([ - factoryAddress, - initFunc - ]) - const paymasterAndData = pmRule == null ? '0x' : hexConcat([paymaster.address, Buffer.from(pmRule)]) - const signature = hexlify(Buffer.from(validateRule)) + const initCode = hexConcat([factoryAddress, initFunc]); + const paymasterAndData = + pmRule == null + ? '0x' + : hexConcat([paymaster.address, Buffer.from(pmRule)]); + const signature = hexlify(Buffer.from(validateRule)); const callinitCodeForAddr = await provider.call({ to: factoryAddress, - data: initFunc - }) + data: initFunc, + }); // todo: why "call" above doesn't throw on error ?!?! if (decodeErrorReason(callinitCodeForAddr)?.message != null) { - throw new Error(decodeErrorReason(callinitCodeForAddr)?.message) + throw new Error(decodeErrorReason(callinitCodeForAddr)?.message); } - const [sender] = defaultAbiCoder.decode(['address'], callinitCodeForAddr) + const [sender] = defaultAbiCoder.decode(['address'], callinitCodeForAddr); return { ...cEmptyUserOp, sender, @@ -108,158 +165,208 @@ describe('#ValidationManager', () => { paymasterAndData, callGasLimit: 1e6, verificationGasLimit: 1e6, - preVerificationGas: 50000 - } + preVerificationGas: 50000, + }; } - const provider = ethers.provider - const ethersSigner = provider.getSigner() + const { provider } = ethers; + const ethersSigner = provider.getSigner(); before(async function () { - entryPoint = await new EntryPoint__factory(ethersSigner).deploy() - paymaster = await new TestOpcodesAccount__factory(ethersSigner).deploy() - await entryPoint.depositTo(paymaster.address, { value: parseEther('0.1') }) - await paymaster.addStake(entryPoint.address, { value: parseEther('0.1') }) - opcodeFactory = await new TestOpcodesAccountFactory__factory(ethersSigner).deploy() - testcoin = await new TestCoin__factory(ethersSigner).deploy() - storageFactory = await new TestStorageAccountFactory__factory(ethersSigner).deploy(testcoin.address) - - storageAccount = TestStorageAccount__factory.connect(await storageFactory.callStatic.create(1, ''), provider) - await storageFactory.create(1, '') - - const rulesFactory = await new TestRulesAccountFactory__factory(ethersSigner).deploy() - rulesAccount = TestRulesAccount__factory.connect(await rulesFactory.callStatic.create(''), provider) - await rulesFactory.create('') - await entryPoint.depositTo(rulesAccount.address, { value: parseEther('1') }) - - const reputationManager = new ReputationManager(provider, { - minInclusionDenominator: 1, - throttlingSlack: 1, - banSlack: 1 - }, - parseEther('0'), 0) - const unsafe = !await supportsDebugTraceCall(provider) - vm = new ValidationManager(entryPoint, reputationManager, unsafe) - - if (!await supportsDebugTraceCall(ethers.provider)) { - console.log('WARNING: opcode banning tests can only run with geth') - this.skip() + entryPoint = await new EntryPoint__factory(ethersSigner).deploy(); + paymaster = await new TestOpcodesAccount__factory(ethersSigner).deploy(); + await entryPoint.depositTo(paymaster.address, { value: parseEther('0.1') }); + await paymaster.addStake(entryPoint.address, { value: parseEther('0.1') }); + opcodeFactory = await new TestOpcodesAccountFactory__factory( + ethersSigner, + ).deploy(); + testcoin = await new TestCoin__factory(ethersSigner).deploy(); + storageFactory = await new TestStorageAccountFactory__factory( + ethersSigner, + ).deploy(testcoin.address); + + storageAccount = TestStorageAccount__factory.connect( + await storageFactory.callStatic.create(1, ''), + provider, + ); + await storageFactory.create(1, ''); + + const rulesFactory = await new TestRulesAccountFactory__factory( + ethersSigner, + ).deploy(); + rulesAccount = TestRulesAccount__factory.connect( + await rulesFactory.callStatic.create(''), + provider, + ); + await rulesFactory.create(''); + await entryPoint.depositTo(rulesAccount.address, { + value: parseEther('1'), + }); + + const reputationManager = new ReputationManager( + provider, + { + minInclusionDenominator: 1, + throttlingSlack: 1, + banSlack: 1, + }, + parseEther('0'), + 0, + ); + const unsafe = !(await supportsDebugTraceCall(provider)); + vm = new ValidationManager(entryPoint, reputationManager, unsafe); + + if (!(await supportsDebugTraceCall(ethers.provider))) { + console.log('WARNING: opcode banning tests can only run with geth'); + this.skip(); } - }) + }); it('#getCodeHashes', async () => { - const epHash = keccak256(await provider.getCode(entryPoint.address)) - const pmHash = keccak256(await provider.getCode(paymaster.address)) - const addresses = [entryPoint.address, paymaster.address] - const packed = defaultAbiCoder.encode(['bytes32[]'], [[epHash, pmHash]]) - const packedHash = keccak256(packed) + const epHash = keccak256(await provider.getCode(entryPoint.address)); + const pmHash = keccak256(await provider.getCode(paymaster.address)); + const addresses = [entryPoint.address, paymaster.address]; + const packed = defaultAbiCoder.encode(['bytes32[]'], [[epHash, pmHash]]); + const packedHash = keccak256(packed); expect(await vm.getCodeHashes(addresses)).to.eql({ addresses, - hash: packedHash - }) - }) + hash: packedHash, + }); + }); it('should accept plain request', async () => { - await testUserOp() - }) + await testUserOp(); + }); it('test sanity: reject unknown rule', async () => { - expect(await testUserOp('') - .catch(e => e.message)).to.match(/unknown rule/) - }) + expect(await testUserOp('').catch((e) => e.message)).to.match( + /unknown rule/, + ); + }); it('should fail with bad opcode in ctr', async () => { expect( - await testUserOp('', undefined, opcodeFactory.interface.encodeFunctionData('create', ['coinbase'])) - .catch(e => e.message)).to.match(/factory uses banned opcode: COINBASE/) - }) + await testUserOp( + '', + undefined, + opcodeFactory.interface.encodeFunctionData('create', ['coinbase']), + ).catch((e) => e.message), + ).to.match(/factory uses banned opcode: COINBASE/); + }); it('should fail with bad opcode in paymaster', async () => { - expect(await testUserOp('', 'coinbase', undefined) - .catch(e => e.message)).to.match(/paymaster uses banned opcode: COINBASE/) - }) + expect( + await testUserOp('', 'coinbase', undefined).catch((e) => e.message), + ).to.match(/paymaster uses banned opcode: COINBASE/); + }); it('should fail with bad opcode in validation', async () => { - expect(await testUserOp('blockhash') - .catch(e => e.message)).to.match(/account uses banned opcode: BLOCKHASH/) - }) + expect(await testUserOp('blockhash').catch((e) => e.message)).to.match( + /account uses banned opcode: BLOCKHASH/, + ); + }); it('should fail if creating too many', async () => { - expect(await testUserOp('create2') - .catch(e => e.message)).to.match(/account uses banned opcode: CREATE2/) - }) + expect(await testUserOp('create2').catch((e) => e.message)).to.match( + /account uses banned opcode: CREATE2/, + ); + }); // TODO: add a test with existing wallet, which should succeed (there is one in the "bundler spec" it('should fail referencing self token balance (during wallet creation)', async () => { - expect(await testUserOp('balance-self', undefined, storageFactory.interface.encodeFunctionData('create', [0, '']), storageFactory.address) - .catch(e => e) - ).to.match(/unstaked account accessed/) - }) + expect( + await testUserOp( + 'balance-self', + undefined, + storageFactory.interface.encodeFunctionData('create', [0, '']), + storageFactory.address, + ).catch((e) => e), + ).to.match(/unstaked account accessed/); + }); it('account succeeds referencing its own balance (after wallet creation)', async () => { - await testExistingUserOp('balance-self') - }) + await testExistingUserOp('balance-self'); + }); describe('access allowance (existing wallet)', () => { it('account fails to read allowance of other address (even if account is token owner)', async () => { - expect(await testExistingUserOp('allowance-self-1') - .catch(e => e.message)) - .to.match(/account has forbidden read/) - }) + expect( + await testExistingUserOp('allowance-self-1').catch((e) => e.message), + ).to.match(/account has forbidden read/); + }); it('account can reference its own allowance on other contract balance', async () => { - await testExistingUserOp('allowance-1-self') - }) - }) + await testExistingUserOp('allowance-1-self'); + }); + }); describe('access struct (existing wallet)', () => { it('should access self struct data', async () => { - await testExistingUserOp('struct-self') - }) + await testExistingUserOp('struct-self'); + }); it('should fail to access other address struct data', async () => { - expect(await testExistingUserOp('struct-1') - .catch(e => e.message) - ).match(/account has forbidden read/) - }) - }) + expect( + await testExistingUserOp('struct-1').catch((e) => e.message), + ).match(/account has forbidden read/); + }); + }); describe('time-range', () => { - let testTimeRangeAccountFactory: TestTimeRangeAccountFactory + let testTimeRangeAccountFactory: TestTimeRangeAccountFactory; // note: parameters are "js time", not "unix time" - async function testTimeRangeUserOp (validAfterMs: number, validUntilMs: number): Promise { - const userOp = await createTestUserOp('', undefined, undefined, testTimeRangeAccountFactory.address) - userOp.preVerificationGas = Math.floor(validAfterMs / 1000) - userOp.maxPriorityFeePerGas = Math.floor(validUntilMs / 1000) - console.log('=== validAfter: ', userOp.preVerificationGas, 'validuntil', userOp.maxPriorityFeePerGas) - await vm.validateUserOp(userOp) + /** + * + * @param validAfterMs + * @param validUntilMs + */ + async function testTimeRangeUserOp( + validAfterMs: number, + validUntilMs: number, + ): Promise { + const userOp = await createTestUserOp( + '', + undefined, + undefined, + testTimeRangeAccountFactory.address, + ); + userOp.preVerificationGas = Math.floor(validAfterMs / 1000); + userOp.maxPriorityFeePerGas = Math.floor(validUntilMs / 1000); + console.log( + '=== validAfter: ', + userOp.preVerificationGas, + 'validuntil', + userOp.maxPriorityFeePerGas, + ); + await vm.validateUserOp(userOp); } before(async () => { - testTimeRangeAccountFactory = await new TestTimeRangeAccountFactory__factory(ethersSigner).deploy() - }) + testTimeRangeAccountFactory = + await new TestTimeRangeAccountFactory__factory(ethersSigner).deploy(); + }); it('should accept request with future validUntil', async () => { - await testTimeRangeUserOp(0, Date.now() + 60000) - }) + await testTimeRangeUserOp(0, Date.now() + 60000); + }); it('should accept request with past validAfter', async () => { - await testTimeRangeUserOp(10000, 0) - }) + await testTimeRangeUserOp(10000, 0); + }); it('should accept request with valid range validAfter..validTo', async () => { - await testTimeRangeUserOp(10000, Date.now() + 60000) - }) + await testTimeRangeUserOp(10000, Date.now() + 60000); + }); it('should reject request with past validUntil', async () => { await expect( - testTimeRangeUserOp(0, Date.now() - 1000) - ).to.be.rejectedWith('already expired') - }) + testTimeRangeUserOp(0, Date.now() - 1000), + ).to.be.rejectedWith('already expired'); + }); it('should reject request with short validUntil', async () => { await expect( - testTimeRangeUserOp(0, Date.now() + 25000) - ).to.be.rejectedWith('expires too soon') - }) + testTimeRangeUserOp(0, Date.now() + 25000), + ).to.be.rejectedWith('expires too soon'); + }); it('should reject request with future validAfter', async () => { - await expect( - testTimeRangeUserOp(Date.now() * 2, 0) - ).to.be.rejectedWith('future ') - }) - }) + await expect(testTimeRangeUserOp(Date.now() * 2, 0)).to.be.rejectedWith( + 'future ', + ); + }); + }); describe('validate storageMap', () => { // let names: { [name: string]: string } @@ -272,111 +379,140 @@ describe('#ValidationManager', () => { // acc: rulesAccount.address, // tok: await rulesAccount.coin() // } - }) + }); it('should return nothing during account creation', async () => { - const ret = await testUserOp('read-self', undefined, storageFactory.interface.encodeFunctionData('create', [0, '']), storageFactory.address) + const ret = await testUserOp( + 'read-self', + undefined, + storageFactory.interface.encodeFunctionData('create', [0, '']), + storageFactory.address, + ); // console.log('resolved=', resolveNames(ret, names, true)) expect(ret.storageMap[ret.userOp.sender.toLowerCase()]).to.eql({ - [toBytes32(1)]: toBytes32(0) - }) - }) + [toBytes32(1)]: toBytes32(0), + }); + }); it('should return self storage on existing account', async () => { - const ret = await testExistingUserOp('read-self') + const ret = await testExistingUserOp('read-self'); // console.log('resolved=', resolveNames(ret, names, true)) - const account = ret.userOp.sender.toLowerCase() + const account = ret.userOp.sender.toLowerCase(); expect(ret.storageMap[account]).to.eql({ - [toBytes32(1)]: toBytes32(testcoin.address) - }) - }) + [toBytes32(1)]: toBytes32(testcoin.address), + }); + }); it('should return nothing with no storage access', async () => { - const ret = await testExistingUserOp('') - expect(ret.storageMap).to.eql({}) - }) + const ret = await testExistingUserOp(''); + expect(ret.storageMap).to.eql({}); + }); it('should return referenced storage', async () => { - const ret = await testExistingUserOp('balance-self') + const ret = await testExistingUserOp('balance-self'); // console.log('resolved=', resolveNames(ret, names, true)) - const account = ret.userOp.sender.toLowerCase() + const account = ret.userOp.sender.toLowerCase(); // account's token at slot 1 of account expect(ret.storageMap[account]).to.eql({ - [toBytes32(1)]: toBytes32(testcoin.address) - }) + [toBytes32(1)]: toBytes32(testcoin.address), + }); // token.balances[account] - balances uses slot 0 of token - const hashRef = keccak256(hexConcat([toBytes32(account), toBytes32(0)])) + const hashRef = keccak256(hexConcat([toBytes32(account), toBytes32(0)])); expect(ret.storageMap[testcoin.address.toLowerCase()]).to.eql({ - [hashRef]: toBytes32(0) - }) - }) - }) + [hashRef]: toBytes32(0), + }); + }); + }); it('should fail if referencing other token balance', async () => { - expect(await testUserOp('balance-1', undefined, storageFactory.interface.encodeFunctionData('create', [0, '']), storageFactory.address) - .catch(e => e.message)) - .to.match(/account has forbidden read/) - }) + expect( + await testUserOp( + 'balance-1', + undefined, + storageFactory.interface.encodeFunctionData('create', [0, '']), + storageFactory.address, + ).catch((e) => e.message), + ).to.match(/account has forbidden read/); + }); it('should succeed referencing self token balance after wallet creation', async () => { - await testExistingUserOp('balance-self', undefined) - }) + await testExistingUserOp('balance-self', undefined); + }); it('should fail with unstaked paymaster returning context', async () => { - const pm = await new TestStorageAccount__factory(ethersSigner).deploy() + const pm = await new TestStorageAccount__factory(ethersSigner).deploy(); // await entryPoint.depositTo(pm.address, { value: parseEther('0.1') }) // await pm.addStake(entryPoint.address, { value: parseEther('0.1') }) - const acct = await new TestRecursionAccount__factory(ethersSigner).deploy(entryPoint.address) + const acct = await new TestRecursionAccount__factory(ethersSigner).deploy( + entryPoint.address, + ); const userOp = { ...cEmptyUserOp, sender: acct.address, - paymasterAndData: hexConcat([ - pm.address, - Buffer.from('postOp-context') - ]) - } - expect(await vm.validateUserOp(userOp) - .then(() => 'should fail', e => e.message)) - .to.match(/unstaked paymaster must not return context/) - }) + paymasterAndData: hexConcat([pm.address, Buffer.from('postOp-context')]), + }; + expect( + await vm.validateUserOp(userOp).then( + () => 'should fail', + (e) => e.message, + ), + ).to.match(/unstaked paymaster must not return context/); + }); it('should fail if validation recursively calls handleOps', async () => { - const acct = await new TestRecursionAccount__factory(ethersSigner).deploy(entryPoint.address) + const acct = await new TestRecursionAccount__factory(ethersSigner).deploy( + entryPoint.address, + ); const op: UserOperation = { ...cEmptyUserOp, sender: acct.address, signature: hexlify(Buffer.from('handleOps')), - preVerificationGas: 50000 - } - expect( - await vm.validateUserOp(op) - .catch(e => e.message) - ).to.match(/illegal call into EntryPoint/) - }) + preVerificationGas: 50000, + }; + expect(await vm.validateUserOp(op).catch((e) => e.message)).to.match( + /illegal call into EntryPoint/, + ); + }); it('should succeed with inner revert', async () => { - expect(await testUserOp('inner-revert', undefined, storageFactory.interface.encodeFunctionData('create', [0, '']), storageFactory.address)) - }) + expect( + await testUserOp( + 'inner-revert', + undefined, + storageFactory.interface.encodeFunctionData('create', [0, '']), + storageFactory.address, + ), + ); + }); it('should fail with inner oog revert', async () => { - expect(await testUserOp('oog', undefined, storageFactory.interface.encodeFunctionData('create', [0, '']), storageFactory.address) - .catch(e => e.message) - ).to.match(/account internally reverts on oog/) - }) + expect( + await testUserOp( + 'oog', + undefined, + storageFactory.interface.encodeFunctionData('create', [0, '']), + storageFactory.address, + ).catch((e) => e.message), + ).to.match(/account internally reverts on oog/); + }); describe('ValidationPackage', () => { it('should pass for a transaction that does not violate the rules', async () => { - const userOp = await createTestUserOp() - const res = await checkRulesViolations(provider, userOp, entryPoint.address) - assert.equal(res.returnInfo.sigFailed, false) - }) + const userOp = await createTestUserOp(); + const res = await checkRulesViolations( + provider, + userOp, + entryPoint.address, + ); + assert.equal(res.returnInfo.sigFailed, false); + }); it('should throw for a transaction that violates the rules', async () => { - const userOp = await createTestUserOp('coinbase') + const userOp = await createTestUserOp('coinbase'); await expect( - checkRulesViolations(provider, userOp, entryPoint.address) - ).to.be.rejectedWith('account uses banned opcode: COINBASE') - }) - }) -}) + checkRulesViolations(provider, userOp, entryPoint.address), + ).to.be.rejectedWith('account uses banned opcode: COINBASE'); + }); + }); +}); diff --git a/src/bundler/test/moduleUtils.test.ts b/src/bundler/test/moduleUtils.test.ts index 124c1df..16ecc20 100644 --- a/src/bundler/test/moduleUtils.test.ts +++ b/src/bundler/test/moduleUtils.test.ts @@ -1,27 +1,34 @@ -import { expect } from 'chai' -import { mergeStorageMap } from '../../utils' +import { expect } from 'chai'; + +import { mergeStorageMap } from '../../utils'; describe('#moduleUtils', () => { describe('#mergeStorageMap', () => { it('merge item into empty map', () => { - expect(mergeStorageMap({}, { a: 'val' })) - .to.eql({ a: 'val' }) - }) + expect(mergeStorageMap({}, { a: 'val' })).to.eql({ a: 'val' }); + }); it('merge items', () => { - expect(mergeStorageMap({ a: 'vala', b: 'valb' }, { a: 'val', c: 'valc' })) - .to.eql({ a: 'val', b: 'valb', c: 'valc' }) - }) + expect( + mergeStorageMap({ a: 'vala', b: 'valb' }, { a: 'val', c: 'valc' }), + ).to.eql({ a: 'val', b: 'valb', c: 'valc' }); + }); it('merge storage cells', () => { - expect(mergeStorageMap({ a: { s1: 's1', s2: 'v2' } }, { a: { s1: 's1', s3: 'v3' } })) - .to.eql({ a: { s1: 's1', s2: 'v2', s3: 'v3' } }) - }) + expect( + mergeStorageMap( + { a: { s1: 's1', s2: 'v2' } }, + { a: { s1: 's1', s3: 'v3' } }, + ), + ).to.eql({ a: { s1: 's1', s2: 'v2', s3: 'v3' } }); + }); it('should prefer root over slots in merged', async () => { - expect(mergeStorageMap({ a: 'aa1' }, { a: { s1: 's1', s3: 'v3' } })) - .to.eql({ a: 'aa1' }) - }) + expect( + mergeStorageMap({ a: 'aa1' }, { a: { s1: 's1', s3: 'v3' } }), + ).to.eql({ a: 'aa1' }); + }); it('should prefer root over slots in validateStorage', async () => { - expect(mergeStorageMap({ a: { s1: 's1', s3: 'v3' } }, { a: 'aa1' })) - .to.eql({ a: 'aa1' }) - }) - }) -}) + expect( + mergeStorageMap({ a: { s1: 's1', s3: 'v3' } }, { a: 'aa1' }), + ).to.eql({ a: 'aa1' }); + }); + }); +}); diff --git a/src/bundler/test/opcodes.test.ts b/src/bundler/test/opcodes.test.ts index 29a074e..6f3fc6b 100644 --- a/src/bundler/test/opcodes.test.ts +++ b/src/bundler/test/opcodes.test.ts @@ -1,3 +1 @@ -describe('opcode banning', () => { - -}) +describe('opcode banning', () => {}); diff --git a/src/bundler/test/runBundler.test.ts b/src/bundler/test/runBundler.test.ts index d9b9897..670fb07 100644 --- a/src/bundler/test/runBundler.test.ts +++ b/src/bundler/test/runBundler.test.ts @@ -1,3 +1,3 @@ describe('runBundler', function () { // it('resolveConfiguration') -}) +}); diff --git a/src/bundler/test/testUtils.ts b/src/bundler/test/testUtils.ts index 7d2cca8..2c29398 100644 --- a/src/bundler/test/testUtils.ts +++ b/src/bundler/test/testUtils.ts @@ -1,43 +1,63 @@ -import { BigNumber, Signer, Wallet } from 'ethers' -import { HDNode, parseEther } from 'ethers/lib/utils' -import { ethers } from 'hardhat' +import type { Signer } from 'ethers'; +import { BigNumber, Wallet } from 'ethers'; +import { HDNode, parseEther } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; // create an hdkey signer, and fund it, if needed. -export async function createSigner (): Promise { - const provider = ethers.provider - const privateKey = HDNode.fromMnemonic('test '.repeat(11) + 'junk') - const signer = new Wallet(privateKey, provider) - const signerAddress = await signer.getAddress() - const signerBalance = await signer.getBalance() +/** + * + */ +export async function createSigner(): Promise { + const { provider } = ethers; + const privateKey = HDNode.fromMnemonic(`${'test '.repeat(11)}junk`); + const signer = new Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + const signerBalance = await signer.getBalance(); if (signerBalance.lt(parseEther('10'))) { await ethers.provider.getSigner().sendTransaction({ to: signerAddress, - value: parseEther('10') - }) + value: parseEther('10'), + }); } - return signer + return signer; } // debugging helper: // process json object, and convert any key or value that is a hex address into its name // -export function resolveNames (json: T, nameToAddress: { [name: string]: string }, onlyNames = false): T { - const addressToNameMap: { [addr: string]: string } = Object.entries(nameToAddress) - .reduce((set, [name, addr]) => ({ +/** + * + * @param json + * @param onlyNames + */ +export function resolveNames( + json: T, + nameToAddress: { [name: string]: string }, + onlyNames = false, +): T { + const addressToNameMap: { [addr: string]: string } = Object.entries( + nameToAddress, + ).reduce( + (set, [name, addr]) => ({ ...set, - [addr.toLowerCase().replace(/0x0*/, '')]: name - }), {}) - const s = JSON.stringify(json) + [addr.toLowerCase().replace(/0x0*/, '')]: name, + }), + {}, + ); + const s = JSON.stringify(json); const s1 = s - .replace(/[{]"type":"BigNumber","hex":"(.*?)"[}]/g, (_, hex) => BigNumber.from(hex).toString()) + .replace(/[{]"type":"BigNumber","hex":"(.*?)"[}]/g, (_, hex) => + BigNumber.from(hex).toString(), + ) .replace(/(0x0*)([0-9a-fA-F]+)+/g, (_, prefix: string, hex: string) => { - const hexToName = addressToNameMap[hex.toLowerCase()] - if (hexToName == null) return `${prefix}${hex}` // not found in map: leave as-is + const hexToName = addressToNameMap[hex.toLowerCase()]; + if (hexToName == null) { + return `${prefix}${hex}`; + } // not found in map: leave as-is if (onlyNames) { - return hexToName - } else { - return `${prefix}<${hexToName}>${hex}` + return hexToName; } - }) - return JSON.parse(s1) + return `${prefix}<${hexToName}>${hex}`; + }); + return JSON.parse(s1); } diff --git a/src/bundler/test/tracer.test.ts b/src/bundler/test/tracer.test.ts index 7a4bdee..077c895 100644 --- a/src/bundler/test/tracer.test.ts +++ b/src/bundler/test/tracer.test.ts @@ -1,99 +1,155 @@ -import { TracerTest, TracerTest__factory } from '../../utils' -import { ethers } from 'hardhat' -import { debug_traceCall } from '../../validation-manager/GethTracer' -import { expect } from 'chai' -import { BundlerTracerResult, bundlerCollectorTracer } from '../../validation-manager/BundlerCollectorTracer' -import { BytesLike } from 'ethers' +import { expect } from 'chai'; +import type { BytesLike } from 'ethers'; +import { ethers } from 'hardhat'; -const provider = ethers.provider -const signer = provider.getSigner() +import type { TracerTest } from '../../utils'; +import { TracerTest__factory } from '../../utils'; +import type { BundlerTracerResult } from '../../validation-manager/BundlerCollectorTracer'; +import { bundlerCollectorTracer } from '../../validation-manager/BundlerCollectorTracer'; +import { debug_traceCall } from '../../validation-manager/GethTracer'; + +const { provider } = ethers; +const signer = provider.getSigner(); describe('#bundlerCollectorTracer', () => { - let tester: TracerTest + let tester: TracerTest; before(async function () { - const ver: string = await (provider as any).send('web3_clientVersion') + const ver: string = await (provider as any).send('web3_clientVersion'); if (ver.match('go1') == null) { - console.warn('\t==WARNING: test requires debug_traceCall on Geth (go-ethereum) node. ver=' + ver) - this.skip() - return + console.warn( + `\t==WARNING: test requires debug_traceCall on Geth (go-ethereum) node. ver=${ver}`, + ); + this.skip(); + return; } - tester = await new TracerTest__factory(signer).deploy() - await tester.deployTransaction.wait() - }) + tester = await new TracerTest__factory(signer).deploy(); + await tester.deployTransaction.wait(); + }); it('should count opcodes on depth>1', async () => { - const ret = await traceExecSelf(tester.interface.encodeFunctionData('callTimeStamp'), false, true) - const execEvent = tester.interface.decodeEventLog('ExecSelfResult', ret.logs[0].data, ret.logs[0].topics) - expect(execEvent.success).to.equal(true) - expect(ret.callsFromEntryPoint[0].opcodes.TIMESTAMP).to.equal(1) - }) + const ret = await traceExecSelf( + tester.interface.encodeFunctionData('callTimeStamp'), + false, + true, + ); + const execEvent = tester.interface.decodeEventLog( + 'ExecSelfResult', + ret.logs[0].data, + ret.logs[0].topics, + ); + expect(execEvent.success).to.equal(true); + expect(ret.callsFromEntryPoint[0].opcodes.TIMESTAMP).to.equal(1); + }); it('should not count opcodes on depth==1', async () => { - const ret = await traceCall(tester.interface.encodeFunctionData('callTimeStamp')) - expect(ret.callsFromEntryPoint[0]?.opcodes.TIMESTAMP).to.be.undefined + const ret = await traceCall( + tester.interface.encodeFunctionData('callTimeStamp'), + ); + expect(ret.callsFromEntryPoint[0]?.opcodes.TIMESTAMP).to.be.undefined; // verify no error.. - expect(ret.debug.toString()).to.not.match(/REVERT/) - }) + expect(ret.debug.toString()).to.not.match(/REVERT/); + }); - async function traceCall (functionData: BytesLike): Promise { - const ret: BundlerTracerResult = await debug_traceCall(provider, { - to: tester.address, - data: functionData - }, { - tracer: bundlerCollectorTracer - }) - return ret + /** + * + * @param functionData + */ + async function traceCall( + functionData: BytesLike, + ): Promise { + const ret: BundlerTracerResult = await debug_traceCall( + provider, + { + to: tester.address, + data: functionData, + }, + { + tracer: bundlerCollectorTracer, + }, + ); + return ret; } // wrap call in a call to self (depth+1) - async function traceExecSelf (functionData: BytesLike, useNumber = true, extraWrapper = false): Promise { - const execTestCallGas = tester.interface.encodeFunctionData('execSelf', [functionData, useNumber]) + /** + * + * @param functionData + * @param useNumber + * @param extraWrapper + */ + async function traceExecSelf( + functionData: BytesLike, + useNumber = true, + extraWrapper = false, + ): Promise { + const execTestCallGas = tester.interface.encodeFunctionData('execSelf', [ + functionData, + useNumber, + ]); if (extraWrapper) { // add another wreapper for "execSelf" (since our tracer doesn't collect stuff from top-level method - return await traceExecSelf(execTestCallGas, useNumber, false) + return await traceExecSelf(execTestCallGas, useNumber, false); } - const ret = await traceCall(execTestCallGas) - return ret + const ret = await traceCall(execTestCallGas); + return ret; } describe('#traceExecSelf', () => { it('should revert', async () => { - const ret = await traceExecSelf('0xdead', true, true) - expect(ret.debug.toString()).to.match(/execution reverted/) - expect(ret.logs.length).to.equal(1) - const log = tester.interface.decodeEventLog('ExecSelfResult', ret.logs[0].data, ret.logs[0].topics) - expect(log.success).to.equal(false) - }) + const ret = await traceExecSelf('0xdead', true, true); + expect(ret.debug.toString()).to.match(/execution reverted/); + expect(ret.logs.length).to.equal(1); + const log = tester.interface.decodeEventLog( + 'ExecSelfResult', + ret.logs[0].data, + ret.logs[0].topics, + ); + expect(log.success).to.equal(false); + }); it('should call itself', async () => { // sanity check: execSelf works and call itself (even recursively) - const innerCall = tester.interface.encodeFunctionData('doNothing') - const execInner = tester.interface.encodeFunctionData('execSelf', [innerCall, false]) - const ret = await traceExecSelf(execInner, true, true) - expect(ret.logs.length).to.equal(2) - ret.logs.forEach(log => { - const logParams = tester.interface.decodeEventLog('ExecSelfResult', log.data, log.topics) - expect(logParams.success).to.equal(true) - }) - }) - }) + const innerCall = tester.interface.encodeFunctionData('doNothing'); + const execInner = tester.interface.encodeFunctionData('execSelf', [ + innerCall, + false, + ]); + const ret = await traceExecSelf(execInner, true, true); + expect(ret.logs.length).to.equal(2); + ret.logs.forEach((log) => { + const logParams = tester.interface.decodeEventLog( + 'ExecSelfResult', + log.data, + log.topics, + ); + expect(logParams.success).to.equal(true); + }); + }); + }); it('should report direct use of GAS opcode', async () => { - const ret = await traceExecSelf(tester.interface.encodeFunctionData('testCallGas'), false) - expect(ret.callsFromEntryPoint['0'].opcodes.GAS).to.eq(1) - }) + const ret = await traceExecSelf( + tester.interface.encodeFunctionData('testCallGas'), + false, + ); + expect(ret.callsFromEntryPoint['0'].opcodes.GAS).to.eq(1); + }); it('should ignore gas used as part of "call"', async () => { // call the "testKeccak" function as a sample inner function - const doNothing = tester.interface.encodeFunctionData('doNothing') - const callDoNothing = tester.interface.encodeFunctionData('execSelf', [doNothing, false]) - const ret = await traceExecSelf(callDoNothing, false) - expect(ret.callsFromEntryPoint['0'].opcodes.GAS).to.be.undefined - }) + const doNothing = tester.interface.encodeFunctionData('doNothing'); + const callDoNothing = tester.interface.encodeFunctionData('execSelf', [ + doNothing, + false, + ]); + const ret = await traceExecSelf(callDoNothing, false); + expect(ret.callsFromEntryPoint['0'].opcodes.GAS).to.be.undefined; + }); it('should collect traces only until BeginExecution event', async () => { // the method calls "callTimeStamp" 3 times, but should stop tracing after 2 times.. - const callStopTracing = tester.interface.encodeFunctionData('testStopTracing') - const ret = await traceCall(callStopTracing) - expect(ret.callsFromEntryPoint.length).to.eql(2) - }) -}) + const callStopTracing = + tester.interface.encodeFunctionData('testStopTracing'); + const ret = await traceCall(callStopTracing); + expect(ret.callsFromEntryPoint.length).to.eql(2); + }); +}); diff --git a/src/bundler/test/utils.test.ts b/src/bundler/test/utils.test.ts index a381139..86e41bd 100644 --- a/src/bundler/test/utils.test.ts +++ b/src/bundler/test/utils.test.ts @@ -1,46 +1,57 @@ -import { expect } from 'chai' -import { BigNumber } from 'ethers' -import { deepHexlify } from '../../utils' +import { expect } from 'chai'; +import { BigNumber } from 'ethers'; + +import { deepHexlify } from '../../utils'; describe('#deepHexlify', function () { it('empty', () => { - expect(deepHexlify({})).to.eql({}) - }) + expect(deepHexlify({})).to.eql({}); + }); it('flat', () => { - expect(deepHexlify({ a: 1 })).to.eql({ a: '0x1' }) - }) + expect(deepHexlify({ a: 1 })).to.eql({ a: '0x1' }); + }); it('no-modify for strings', () => { - expect(deepHexlify({ a: 'hello' })).to.eql({ a: 'hello' }) - }) + expect(deepHexlify({ a: 'hello' })).to.eql({ a: 'hello' }); + }); it('no-modify for boolean', () => { - expect(deepHexlify({ a: false })).to.eql({ a: false }) - }) + expect(deepHexlify({ a: false })).to.eql({ a: false }); + }); it('bignum', () => { - expect(deepHexlify({ a: BigNumber.from(3) })).to.eql({ a: '0x3' }) - }) + expect(deepHexlify({ a: BigNumber.from(3) })).to.eql({ a: '0x3' }); + }); it('deep object ', () => { - expect(deepHexlify({ - a: 1, - b: { - c: 4, - d: false, - e: [{ - f: 5, - g: 'nothing', - h: true - }, 2, 3] - } - })).to.eql({ + expect( + deepHexlify({ + a: 1, + b: { + c: 4, + d: false, + e: [ + { + f: 5, + g: 'nothing', + h: true, + }, + 2, + 3, + ], + }, + }), + ).to.eql({ a: '0x1', b: { c: '0x4', d: false, - e: [{ - f: '0x5', - g: 'nothing', - h: true - }, '0x2', '0x3'] - } - }) - }) -}) + e: [ + { + f: '0x5', + g: 'nothing', + h: true, + }, + '0x2', + '0x3', + ], + }, + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index f41247f..0418d9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export { runBundler } from './bundler/runBundler' -export { BundlerServer } from './bundler/BundlerServer' +export { runBundler } from './bundler/runBundler'; +export { BundlerServer } from './bundler/BundlerServer'; diff --git a/src/sdk/BaseAccountAPI.ts b/src/sdk/BaseAccountAPI.ts index ac3cd83..2039dc0 100644 --- a/src/sdk/BaseAccountAPI.ts +++ b/src/sdk/BaseAccountAPI.ts @@ -1,28 +1,32 @@ -import { ethers, BigNumber, BigNumberish } from 'ethers' -import { Provider } from '@ethersproject/providers' -import { - EntryPoint, EntryPoint__factory, - UserOperationStruct -} from '@account-abstraction/contracts' - -import { TransactionDetailsForUserOp } from './TransactionDetailsForUserOp' -import { resolveProperties } from 'ethers/lib/utils' -import { PaymasterAPI } from './PaymasterAPI' -import { getUserOpHash, NotPromise, packUserOp } from '../utils' -import { calcPreVerificationGas, GasOverheads } from './calcPreVerificationGas' - -export interface BaseApiParams { - provider: Provider - entryPointAddress: string - accountAddress?: string - overheads?: Partial - paymasterAPI?: PaymasterAPI -} - -export interface UserOpResult { - transactionHash: string - success: boolean -} +import type { + EntryPoint, + UserOperationStruct, +} from '@account-abstraction/contracts'; +import { EntryPoint__factory } from '@account-abstraction/contracts'; +import type { Provider } from '@ethersproject/providers'; +import type { BigNumberish } from 'ethers'; +import { ethers, BigNumber } from 'ethers'; +import { resolveProperties } from 'ethers/lib/utils'; + +import type { GasOverheads } from './calcPreVerificationGas'; +import { calcPreVerificationGas } from './calcPreVerificationGas'; +import type { PaymasterAPI } from './PaymasterAPI'; +import type { TransactionDetailsForUserOp } from './TransactionDetailsForUserOp'; +import type { NotPromise } from '../utils'; +import { getUserOpHash, packUserOp } from '../utils'; + +export type BaseApiParams = { + provider: Provider; + entryPointAddress: string; + accountAddress?: string; + overheads?: Partial; + paymasterAPI?: PaymasterAPI; +}; + +export type UserOpResult = { + transactionHash: string; + success: boolean; +}; /** * Base class for all Smart Wallet ERC-4337 Clients to implement. @@ -38,51 +42,61 @@ export interface UserOpResult { * - createSignedUserOp - helper to call the above createUnsignedUserOp, and then extract the userOpHash and sign it */ export abstract class BaseAccountAPI { - private senderAddress!: string - private isPhantom = true + private senderAddress!: string; + + private isPhantom = true; + // entryPoint connected to "zero" address. allowed to make static calls (e.g. to getSenderAddress) - private readonly entryPointView: EntryPoint + private readonly entryPointView: EntryPoint; + + provider: Provider; - provider: Provider - overheads?: Partial - entryPointAddress: string - accountAddress?: string - paymasterAPI?: PaymasterAPI + overheads?: Partial; + + entryPointAddress: string; + + accountAddress?: string; + + paymasterAPI?: PaymasterAPI; /** * base constructor. * subclass SHOULD add parameters that define the owner (signer) of this wallet + * @param params */ - protected constructor (params: BaseApiParams) { - this.provider = params.provider - this.overheads = params.overheads - this.entryPointAddress = params.entryPointAddress - this.accountAddress = params.accountAddress - this.paymasterAPI = params.paymasterAPI + protected constructor(params: BaseApiParams) { + this.provider = params.provider; + this.overheads = params.overheads; + this.entryPointAddress = params.entryPointAddress; + this.accountAddress = params.accountAddress; + this.paymasterAPI = params.paymasterAPI; // factory "connect" define the contract address. the contract "connect" defines the "from" address. - this.entryPointView = EntryPoint__factory.connect(params.entryPointAddress, params.provider).connect(ethers.constants.AddressZero) + this.entryPointView = EntryPoint__factory.connect( + params.entryPointAddress, + params.provider, + ).connect(ethers.constants.AddressZero); } - async init (): Promise { - if (await this.provider.getCode(this.entryPointAddress) === '0x') { - throw new Error(`entryPoint not deployed at ${this.entryPointAddress}`) + async init(): Promise { + if ((await this.provider.getCode(this.entryPointAddress)) === '0x') { + throw new Error(`entryPoint not deployed at ${this.entryPointAddress}`); } - await this.getAccountAddress() - return this + await this.getAccountAddress(); + return this; } /** * return the value to put into the "initCode" field, if the contract is not yet deployed. * this value holds the "factory" address, followed by this account's information */ - abstract getAccountInitCode (): Promise + abstract getAccountInitCode(): Promise; /** * return current account's nonce. */ - abstract getNonce (): Promise + abstract getNonce(): Promise; /** * encode the call from entryPoint through our account to the target contract. @@ -90,137 +104,166 @@ export abstract class BaseAccountAPI { * @param value * @param data */ - abstract encodeExecute (target: string, value: BigNumberish, data: string): Promise + abstract encodeExecute( + target: string, + value: BigNumberish, + data: string, + ): Promise; /** * sign a userOp's hash (userOpHash). * @param userOpHash */ - abstract signUserOpHash (userOpHash: string): Promise + abstract signUserOpHash(userOpHash: string): Promise; /** * check if the contract is already deployed. */ - async checkAccountPhantom (): Promise { + async checkAccountPhantom(): Promise { if (!this.isPhantom) { // already deployed. no need to check anymore. - return this.isPhantom + return this.isPhantom; } - const senderAddressCode = await this.provider.getCode(this.getAccountAddress()) + const senderAddressCode = await this.provider.getCode( + this.getAccountAddress(), + ); if (senderAddressCode.length > 2) { // console.log(`SimpleAccount Contract already deployed at ${this.senderAddress}`) - this.isPhantom = false + this.isPhantom = false; } else { // console.log(`SimpleAccount Contract is NOT YET deployed at ${this.senderAddress} - working in "phantom account" mode.`) } - return this.isPhantom + return this.isPhantom; } /** * calculate the account address even before it is deployed */ - async getCounterFactualAddress (): Promise { - const initCode = this.getAccountInitCode() + async getCounterFactualAddress(): Promise { + const initCode = this.getAccountInitCode(); // use entryPoint to query account address (factory can provide a helper method to do the same, but // this method attempts to be generic try { - await this.entryPointView.callStatic.getSenderAddress(initCode) + await this.entryPointView.callStatic.getSenderAddress(initCode); } catch (e: any) { if (e.errorArgs == null) { - throw e + throw e; } - return e.errorArgs.sender + return e.errorArgs.sender; } - throw new Error('must handle revert') + throw new Error('must handle revert'); } /** * return initCode value to into the UserOp. * (either deployment code, or empty hex if contract already deployed) */ - async getInitCode (): Promise { + async getInitCode(): Promise { if (await this.checkAccountPhantom()) { - return await this.getAccountInitCode() + return await this.getAccountInitCode(); } - return '0x' + return '0x'; } /** * return maximum gas used for verification. * NOTE: createUnsignedUserOp will add to this value the cost of creation, if the contract is not yet created. */ - async getVerificationGasLimit (): Promise { - return 100000 + async getVerificationGasLimit(): Promise { + return 100000; } /** * should cover cost of putting calldata on-chain, and some overhead. * actual overhead depends on the expected bundle size + * @param userOp */ - async getPreVerificationGas (userOp: Partial): Promise { - const p = await resolveProperties(userOp) - return calcPreVerificationGas(p, this.overheads) + async getPreVerificationGas( + userOp: Partial, + ): Promise { + const p = await resolveProperties(userOp); + return calcPreVerificationGas(p, this.overheads); } /** * ABI-encode a user operation. used for calldata cost estimation + * @param userOp */ - packUserOp (userOp: NotPromise): string { - return packUserOp(userOp, false) + packUserOp(userOp: NotPromise): string { + return packUserOp(userOp, false); } - async encodeUserOpCallDataAndGasLimit (detailsForUserOp: TransactionDetailsForUserOp): Promise<{ callData: string, callGasLimit: BigNumber }> { - function parseNumber (a: any): BigNumber | null { - if (a == null || a === '') return null - return BigNumber.from(a.toString()) + async encodeUserOpCallDataAndGasLimit( + detailsForUserOp: TransactionDetailsForUserOp, + ): Promise<{ callData: string; callGasLimit: BigNumber }> { + /** + * + * @param a + */ + function parseNumber(a: any): BigNumber | null { + if (a == null || a === '') { + return null; + } + return BigNumber.from(a.toString()); } - const value = parseNumber(detailsForUserOp.value) ?? BigNumber.from(0) - const callData = await this.encodeExecute(detailsForUserOp.target, value, detailsForUserOp.data) - - const callGasLimit = parseNumber(detailsForUserOp.gasLimit) ?? await this.provider.estimateGas({ - from: this.entryPointAddress, - to: this.getAccountAddress(), - data: callData - }) + const value = parseNumber(detailsForUserOp.value) ?? BigNumber.from(0); + const callData = await this.encodeExecute( + detailsForUserOp.target, + value, + detailsForUserOp.data, + ); + + const callGasLimit = + parseNumber(detailsForUserOp.gasLimit) ?? + (await this.provider.estimateGas({ + from: this.entryPointAddress, + to: this.getAccountAddress(), + data: callData, + })); return { callData, - callGasLimit - } + callGasLimit, + }; } /** * return userOpHash for signing. * This value matches entryPoint.getUserOpHash (calculated off-chain, to avoid a view call) - * @param userOp userOperation, (signature field ignored) + * @param userOp - userOperation, (signature field ignored) */ - async getUserOpHash (userOp: UserOperationStruct): Promise { - const op = await resolveProperties(userOp) - const chainId = await this.provider.getNetwork().then(net => net.chainId) - return getUserOpHash(op, this.entryPointAddress, chainId) + async getUserOpHash(userOp: UserOperationStruct): Promise { + const op = await resolveProperties(userOp); + const chainId = await this.provider.getNetwork().then((net) => net.chainId); + return getUserOpHash(op, this.entryPointAddress, chainId); } /** * return the account's address. * this value is valid even before deploying the contract. */ - async getAccountAddress (): Promise { + async getAccountAddress(): Promise { if (this.senderAddress == null) { if (this.accountAddress != null) { - this.senderAddress = this.accountAddress + this.senderAddress = this.accountAddress; } else { - this.senderAddress = await this.getCounterFactualAddress() + this.senderAddress = await this.getCounterFactualAddress(); } } - return this.senderAddress + return this.senderAddress; } - async estimateCreationGas (initCode?: string): Promise { - if (initCode == null || initCode === '0x') return 0 - const deployerAddress = initCode.substring(0, 42) - const deployerCallData = '0x' + initCode.substring(42) - return await this.provider.estimateGas({ to: deployerAddress, data: deployerCallData }) + async estimateCreationGas(initCode?: string): Promise { + if (initCode == null || initCode === '0x') { + return 0; + } + const deployerAddress = initCode.substring(0, 42); + const deployerCallData = `0x${initCode.substring(42)}`; + return await this.provider.estimateGas({ + to: deployerAddress, + data: deployerCallData, + }); } /** @@ -229,28 +272,26 @@ export abstract class BaseAccountAPI { * - if gas or nonce are missing, read them from the chain (note that we can't fill gaslimit before the account is created) * @param info */ - async createUnsignedUserOp (info: TransactionDetailsForUserOp): Promise { - const { - callData, - callGasLimit - } = await this.encodeUserOpCallDataAndGasLimit(info) - const initCode = await this.getInitCode() - - const initGas = await this.estimateCreationGas(initCode) - const verificationGasLimit = BigNumber.from(await this.getVerificationGasLimit()) - .add(initGas) - - let { - maxFeePerGas, - maxPriorityFeePerGas - } = info + async createUnsignedUserOp( + info: TransactionDetailsForUserOp, + ): Promise { + const { callData, callGasLimit } = + await this.encodeUserOpCallDataAndGasLimit(info); + const initCode = await this.getInitCode(); + + const initGas = await this.estimateCreationGas(initCode); + const verificationGasLimit = BigNumber.from( + await this.getVerificationGasLimit(), + ).add(initGas); + + let { maxFeePerGas, maxPriorityFeePerGas } = info; if (maxFeePerGas == null || maxPriorityFeePerGas == null) { - const feeData = await this.provider.getFeeData() + const feeData = await this.provider.getFeeData(); if (maxFeePerGas == null) { - maxFeePerGas = feeData.maxFeePerGas ?? undefined + maxFeePerGas = feeData.maxFeePerGas ?? undefined; } if (maxPriorityFeePerGas == null) { - maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? undefined + maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? undefined; } } @@ -263,63 +304,73 @@ export abstract class BaseAccountAPI { verificationGasLimit, maxFeePerGas, maxPriorityFeePerGas, - paymasterAndData: '0x' - } + paymasterAndData: '0x', + }; - let paymasterAndData: string | undefined + let paymasterAndData: string | undefined; if (this.paymasterAPI != null) { // fill (partial) preVerificationGas (all except the cost of the generated paymasterAndData) const userOpForPm = { ...partialUserOp, - preVerificationGas: await this.getPreVerificationGas(partialUserOp) - } - paymasterAndData = await this.paymasterAPI.getPaymasterAndData(userOpForPm) + preVerificationGas: await this.getPreVerificationGas(partialUserOp), + }; + paymasterAndData = await this.paymasterAPI.getPaymasterAndData( + userOpForPm, + ); } - partialUserOp.paymasterAndData = paymasterAndData ?? '0x' + partialUserOp.paymasterAndData = paymasterAndData ?? '0x'; return { ...partialUserOp, preVerificationGas: this.getPreVerificationGas(partialUserOp), - signature: '' - } + signature: '', + }; } /** * Sign the filled userOp. - * @param userOp the UserOperation to sign (with signature field ignored) + * @param userOp - the UserOperation to sign (with signature field ignored) */ - async signUserOp (userOp: UserOperationStruct): Promise { - const userOpHash = await this.getUserOpHash(userOp) - const signature = this.signUserOpHash(userOpHash) + async signUserOp(userOp: UserOperationStruct): Promise { + const userOpHash = await this.getUserOpHash(userOp); + const signature = this.signUserOpHash(userOpHash); return { ...userOp, - signature - } + signature, + }; } /** * helper method: create and sign a user operation. - * @param info transaction details for the userOp + * @param info - transaction details for the userOp */ - async createSignedUserOp (info: TransactionDetailsForUserOp): Promise { - return await this.signUserOp(await this.createUnsignedUserOp(info)) + async createSignedUserOp( + info: TransactionDetailsForUserOp, + ): Promise { + return await this.signUserOp(await this.createUnsignedUserOp(info)); } /** * get the transaction that has this userOpHash mined, or null if not found - * @param userOpHash returned by sendUserOpToBundler (or by getUserOpHash..) - * @param timeout stop waiting after this timeout - * @param interval time to wait between polls. - * @return the transactionHash this userOp was mined, or null if not found. + * @param userOpHash - returned by sendUserOpToBundler (or by getUserOpHash..) + * @param timeout - stop waiting after this timeout + * @param interval - time to wait between polls. + * @returns the transactionHash this userOp was mined, or null if not found. */ - async getUserOpReceipt (userOpHash: string, timeout = 30000, interval = 5000): Promise { - const endtime = Date.now() + timeout + async getUserOpReceipt( + userOpHash: string, + timeout = 30000, + interval = 5000, + ): Promise { + const endtime = Date.now() + timeout; while (Date.now() < endtime) { - const events = await this.entryPointView.queryFilter(this.entryPointView.filters.UserOperationEvent(userOpHash)) + const events = await this.entryPointView.queryFilter( + this.entryPointView.filters.UserOperationEvent(userOpHash), + ); if (events.length > 0) { - return events[0].transactionHash + return events[0].transactionHash; } - await new Promise(resolve => setTimeout(resolve, interval)) + await new Promise((resolve) => setTimeout(resolve, interval)); } - return null + return null; } } diff --git a/src/sdk/ClientConfig.ts b/src/sdk/ClientConfig.ts index 08400d4..caa7d84 100644 --- a/src/sdk/ClientConfig.ts +++ b/src/sdk/ClientConfig.ts @@ -1,26 +1,26 @@ -import { PaymasterAPI } from './PaymasterAPI' +import type { PaymasterAPI } from './PaymasterAPI'; /** * configuration params for wrapProvider */ -export interface ClientConfig { +export type ClientConfig = { /** * the entry point to use */ - entryPointAddress: string + entryPointAddress: string; /** * url to the bundler */ - bundlerUrl: string + bundlerUrl: string; /** * if set, use this pre-deployed wallet. * (if not set, use getSigner().getAddress() to query the "counterfactual" address of wallet. * you may need to fund this address so the wallet can pay for its own creation) */ - walletAddress?: string + walletAddress?: string; /** * if set, call just before signing. */ - paymasterAPI?: PaymasterAPI -} + paymasterAPI?: PaymasterAPI; +}; diff --git a/src/sdk/DeterministicDeployer.ts b/src/sdk/DeterministicDeployer.ts index c1e439b..37855b6 100644 --- a/src/sdk/DeterministicDeployer.ts +++ b/src/sdk/DeterministicDeployer.ts @@ -1,8 +1,9 @@ -import { BigNumber, BigNumberish, ContractFactory } from 'ethers' -import { hexConcat, hexlify, hexZeroPad, keccak256 } from 'ethers/lib/utils' -import { TransactionRequest } from '@ethersproject/abstract-provider' -import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers' -import { Signer } from '@ethersproject/abstract-signer' +import type { TransactionRequest } from '@ethersproject/abstract-provider'; +import type { Signer } from '@ethersproject/abstract-signer'; +import type { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; +import type { BigNumberish, ContractFactory } from 'ethers'; +import { BigNumber } from 'ethers'; +import { hexConcat, hexlify, hexZeroPad, keccak256 } from 'ethers/lib/utils'; /** * wrapper class for Arachnid's deterministic deployer @@ -11,127 +12,185 @@ import { Signer } from '@ethersproject/abstract-signer' export class DeterministicDeployer { /** * return the address this code will get deployed to. - * @param ctrCode constructor code to pass to CREATE2, or ContractFactory - * @param salt optional salt. defaults to zero + * @param ctrCode - constructor code to pass to CREATE2, or ContractFactory + * @param salt - optional salt. defaults to zero */ - static getAddress (ctrCode: string, salt: BigNumberish): string - static getAddress (ctrCode: string): string - static getAddress (ctrCode: ContractFactory, salt: BigNumberish, params: any[]): string - static getAddress (ctrCode: string | ContractFactory, salt: BigNumberish = 0, params: any[] = []): string { - return DeterministicDeployer.getDeterministicDeployAddress(ctrCode, salt, params) + static getAddress(ctrCode: string, salt: BigNumberish): string; + + static getAddress(ctrCode: string): string; + + static getAddress( + ctrCode: ContractFactory, + salt: BigNumberish, + params: any[], + ): string; + + static getAddress( + ctrCode: string | ContractFactory, + salt: BigNumberish = 0, + params: any[] = [], + ): string { + return DeterministicDeployer.getDeterministicDeployAddress( + ctrCode, + salt, + params, + ); } /** * deploy the contract, unless already deployed - * @param ctrCode constructor code to pass to CREATE2 or ContractFactory - * @param salt optional salt. defaults to zero - * @return the deployed address + * @param ctrCode - constructor code to pass to CREATE2 or ContractFactory + * @param salt - optional salt. defaults to zero + * @returns the deployed address */ - static async deploy (ctrCode: string, salt: BigNumberish): Promise - static async deploy (ctrCode: string): Promise - static async deploy (ctrCode: ContractFactory, salt: BigNumberish, params: any[]): Promise - static async deploy (ctrCode: string | ContractFactory, salt: BigNumberish = 0, params: any[] = []): Promise { - return await DeterministicDeployer.instance.deterministicDeploy(ctrCode, salt, params) + static async deploy(ctrCode: string, salt: BigNumberish): Promise; + + static async deploy(ctrCode: string): Promise; + + static async deploy( + ctrCode: ContractFactory, + salt: BigNumberish, + params: any[], + ): Promise; + + static async deploy( + ctrCode: string | ContractFactory, + salt: BigNumberish = 0, + params: any[] = [], + ): Promise { + return await DeterministicDeployer.instance.deterministicDeploy( + ctrCode, + salt, + params, + ); } // from: https://github.com/Arachnid/deterministic-deployment-proxy - static proxyAddress = '0x4e59b44847b379578588920ca78fbf26c0b4956c' - static deploymentTransaction = '0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222' - static deploymentSignerAddress = '0x3fab184622dc19b6109349b94811493bf2a45362' - static deploymentGasPrice = 100e9 - static deploymentGasLimit = 100000 - - constructor ( - readonly provider: JsonRpcProvider, - readonly signer?: Signer) { - } + static proxyAddress = '0x4e59b44847b379578588920ca78fbf26c0b4956c'; + + static deploymentTransaction = + '0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222'; + + static deploymentSignerAddress = '0x3fab184622dc19b6109349b94811493bf2a45362'; - async isContractDeployed (address: string): Promise { - return await this.provider.getCode(address).then(code => code.length > 2) + static deploymentGasPrice = 100e9; + + static deploymentGasLimit = 100000; + + constructor(readonly provider: JsonRpcProvider, readonly signer?: Signer) {} + + async isContractDeployed(address: string): Promise { + return await this.provider.getCode(address).then((code) => code.length > 2); } - async isDeployerDeployed (): Promise { - return await this.isContractDeployed(DeterministicDeployer.proxyAddress) + async isDeployerDeployed(): Promise { + return await this.isContractDeployed(DeterministicDeployer.proxyAddress); } - async deployFactory (): Promise { + async deployFactory(): Promise { if (await this.isContractDeployed(DeterministicDeployer.proxyAddress)) { - return + return; } - const bal = await this.provider.getBalance(DeterministicDeployer.deploymentSignerAddress) - const neededBalance = BigNumber.from(DeterministicDeployer.deploymentGasLimit).mul(DeterministicDeployer.deploymentGasPrice) + const bal = await this.provider.getBalance( + DeterministicDeployer.deploymentSignerAddress, + ); + const neededBalance = BigNumber.from( + DeterministicDeployer.deploymentGasLimit, + ).mul(DeterministicDeployer.deploymentGasPrice); if (bal.lt(neededBalance)) { - const signer = this.signer ?? this.provider.getSigner() + const signer = this.signer ?? this.provider.getSigner(); await signer.sendTransaction({ to: DeterministicDeployer.deploymentSignerAddress, value: neededBalance, - gasLimit: DeterministicDeployer.deploymentGasLimit - }) + gasLimit: DeterministicDeployer.deploymentGasLimit, + }); } - await this.provider.send('eth_sendRawTransaction', [DeterministicDeployer.deploymentTransaction]) - if (!await this.isContractDeployed(DeterministicDeployer.proxyAddress)) { - throw new Error('raw TX didn\'t deploy deployer!') + await this.provider.send('eth_sendRawTransaction', [ + DeterministicDeployer.deploymentTransaction, + ]); + if (!(await this.isContractDeployed(DeterministicDeployer.proxyAddress))) { + throw new Error("raw TX didn't deploy deployer!"); } } - async getDeployTransaction (ctrCode: string | ContractFactory, salt: BigNumberish = 0, params: any[] = []): Promise { - await this.deployFactory() - const saltEncoded = hexZeroPad(hexlify(salt), 32) - const ctrEncoded = DeterministicDeployer.getCtrCode(ctrCode, params) + async getDeployTransaction( + ctrCode: string | ContractFactory, + salt: BigNumberish = 0, + params: any[] = [], + ): Promise { + await this.deployFactory(); + const saltEncoded = hexZeroPad(hexlify(salt), 32); + const ctrEncoded = DeterministicDeployer.getCtrCode(ctrCode, params); return { to: DeterministicDeployer.proxyAddress, - data: hexConcat([ - saltEncoded, - ctrEncoded]) - } + data: hexConcat([saltEncoded, ctrEncoded]), + }; } - static getCtrCode (ctrCode: string | ContractFactory, params: any[]): string { + static getCtrCode(ctrCode: string | ContractFactory, params: any[]): string { if (typeof ctrCode !== 'string') { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return hexlify(ctrCode.getDeployTransaction(...params).data!) - } else { - if (params.length !== 0) { - throw new Error('constructor params can only be passed to ContractFactory') - } - return ctrCode + return hexlify(ctrCode.getDeployTransaction(...params).data!); + } + if (params.length !== 0) { + throw new Error( + 'constructor params can only be passed to ContractFactory', + ); } + return ctrCode; } - static getDeterministicDeployAddress (ctrCode: string | ContractFactory, salt: BigNumberish = 0, params: any[] = []): string { + static getDeterministicDeployAddress( + ctrCode: string | ContractFactory, + salt: BigNumberish = 0, + params: any[] = [], + ): string { // this method works only before the contract is already deployed: // return await this.provider.call(await this.getDeployTransaction(ctrCode, salt)) - const saltEncoded = hexZeroPad(hexlify(salt), 32) - - const ctrCode1 = DeterministicDeployer.getCtrCode(ctrCode, params) - return '0x' + keccak256(hexConcat([ - '0xff', - DeterministicDeployer.proxyAddress, - saltEncoded, - keccak256(ctrCode1) - ])).slice(-40) + const saltEncoded = hexZeroPad(hexlify(salt), 32); + + const ctrCode1 = DeterministicDeployer.getCtrCode(ctrCode, params); + return `0x${keccak256( + hexConcat([ + '0xff', + DeterministicDeployer.proxyAddress, + saltEncoded, + keccak256(ctrCode1), + ]), + ).slice(-40)}`; } - async deterministicDeploy (ctrCode: string | ContractFactory, salt: BigNumberish = 0, params: any[] = []): Promise { - const addr = DeterministicDeployer.getDeterministicDeployAddress(ctrCode, salt, params) - if (!await this.isContractDeployed(addr)) { - const signer = this.signer ?? this.provider.getSigner() + async deterministicDeploy( + ctrCode: string | ContractFactory, + salt: BigNumberish = 0, + params: any[] = [], + ): Promise { + const addr = DeterministicDeployer.getDeterministicDeployAddress( + ctrCode, + salt, + params, + ); + if (!(await this.isContractDeployed(addr))) { + const signer = this.signer ?? this.provider.getSigner(); await signer.sendTransaction( - await this.getDeployTransaction(ctrCode, salt, params)) + await this.getDeployTransaction(ctrCode, salt, params), + ); } - return addr + return addr; } - private static _instance?: DeterministicDeployer + private static _instance?: DeterministicDeployer; - static init (provider: JsonRpcProvider, signer?: JsonRpcSigner): void { - this._instance = new DeterministicDeployer(provider, signer) + static init(provider: JsonRpcProvider, signer?: JsonRpcSigner): void { + this._instance = new DeterministicDeployer(provider, signer); } - static get instance (): DeterministicDeployer { + static get instance(): DeterministicDeployer { if (this._instance == null) { - throw new Error('must call "DeterministicDeployer.init(ethers.provider)" first') + throw new Error( + 'must call "DeterministicDeployer.init(ethers.provider)" first', + ); } - return this._instance + return this._instance; } } diff --git a/src/sdk/ERC4337EthersProvider.ts b/src/sdk/ERC4337EthersProvider.ts index db18ff5..2181d1f 100644 --- a/src/sdk/ERC4337EthersProvider.ts +++ b/src/sdk/ERC4337EthersProvider.ts @@ -1,102 +1,149 @@ -import { BaseProvider, TransactionReceipt, TransactionResponse } from '@ethersproject/providers' -import { BigNumber, Signer } from 'ethers' -import { Network } from '@ethersproject/networks' -import { hexValue, resolveProperties } from 'ethers/lib/utils' +import type { + EntryPoint, + UserOperationStruct, +} from '@account-abstraction/contracts'; +import type { Network } from '@ethersproject/networks'; +import type { + TransactionReceipt, + TransactionResponse, +} from '@ethersproject/providers'; +import { BaseProvider } from '@ethersproject/providers'; +import Debug from 'debug'; +import type { Signer } from 'ethers'; +import { BigNumber } from 'ethers'; +import { hexValue, resolveProperties } from 'ethers/lib/utils'; -import { ClientConfig } from './ClientConfig' -import { ERC4337EthersSigner } from './ERC4337EthersSigner' -import { UserOperationEventListener } from './UserOperationEventListener' -import { HttpRpcClient } from './HttpRpcClient' -import { EntryPoint, UserOperationStruct } from '@account-abstraction/contracts' -import { getUserOpHash } from '../utils' -import { BaseAccountAPI } from './BaseAccountAPI' -import Debug from 'debug' -const debug = Debug('aa.provider') +import type { BaseAccountAPI } from './BaseAccountAPI'; +import type { ClientConfig } from './ClientConfig'; +import { ERC4337EthersSigner } from './ERC4337EthersSigner'; +import type { HttpRpcClient } from './HttpRpcClient'; +import { UserOperationEventListener } from './UserOperationEventListener'; +import { getUserOpHash } from '../utils'; + +const debug = Debug('aa.provider'); export class ERC4337EthersProvider extends BaseProvider { - initializedBlockNumber!: number + initializedBlockNumber!: number; - readonly signer: ERC4337EthersSigner + readonly signer: ERC4337EthersSigner; - constructor ( + constructor( readonly chainId: number, readonly config: ClientConfig, readonly originalSigner: Signer, readonly originalProvider: BaseProvider, readonly httpRpcClient: HttpRpcClient, readonly entryPoint: EntryPoint, - readonly smartAccountAPI: BaseAccountAPI + readonly smartAccountAPI: BaseAccountAPI, ) { super({ name: 'ERC-4337 Custom Network', - chainId - }) - this.signer = new ERC4337EthersSigner(config, originalSigner, this, httpRpcClient, smartAccountAPI) + chainId, + }); + this.signer = new ERC4337EthersSigner( + config, + originalSigner, + this, + httpRpcClient, + smartAccountAPI, + ); } /** * finish intializing the provider. * MUST be called after construction, before using the provider. */ - async init (): Promise { + async init(): Promise { // await this.httpRpcClient.validateChainId() - this.initializedBlockNumber = await this.originalProvider.getBlockNumber() - await this.smartAccountAPI.init() + this.initializedBlockNumber = await this.originalProvider.getBlockNumber(); + await this.smartAccountAPI.init(); // await this.signer.init() - return this + return this; } - getSigner (): ERC4337EthersSigner { - return this.signer + getSigner(): ERC4337EthersSigner { + return this.signer; } - async perform (method: string, params: any): Promise { - debug('perform', method, params) + async perform(method: string, params: any): Promise { + debug('perform', method, params); if (method === 'sendTransaction' || method === 'getTransactionReceipt') { // TODO: do we need 'perform' method to be available at all? // there is nobody out there to use it for ERC-4337 methods yet, we have nothing to override in fact. - throw new Error('Should not get here. Investigate.') + throw new Error('Should not get here. Investigate.'); } - return await this.originalProvider.perform(method, params) + return await this.originalProvider.perform(method, params); } - async getTransaction (transactionHash: string | Promise): Promise { + async getTransaction( + transactionHash: string | Promise, + ): Promise { // TODO - return await super.getTransaction(transactionHash) + return await super.getTransaction(transactionHash); } - async getTransactionReceipt (transactionHash: string | Promise): Promise { - const userOpHash = await transactionHash - const sender = await this.getSenderAccountAddress() + async getTransactionReceipt( + transactionHash: string | Promise, + ): Promise { + const userOpHash = await transactionHash; + const sender = await this.getSenderAccountAddress(); return await new Promise((resolve, reject) => { new UserOperationEventListener( - resolve, reject, this.entryPoint, sender, userOpHash - ).start() - }) + resolve, + reject, + this.entryPoint, + sender, + userOpHash, + ).start(); + }); } - async getSenderAccountAddress (): Promise { - return await this.smartAccountAPI.getAccountAddress() + async getSenderAccountAddress(): Promise { + return await this.smartAccountAPI.getAccountAddress(); } - async waitForTransaction (transactionHash: string, confirmations?: number, timeout?: number): Promise { - const sender = await this.getSenderAccountAddress() + async waitForTransaction( + transactionHash: string, + confirmations?: number, + timeout?: number, + ): Promise { + const sender = await this.getSenderAccountAddress(); return await new Promise((resolve, reject) => { - const listener = new UserOperationEventListener(resolve, reject, this.entryPoint, sender, transactionHash, undefined, timeout) - listener.start() - }) + const listener = new UserOperationEventListener( + resolve, + reject, + this.entryPoint, + sender, + transactionHash, + undefined, + timeout, + ); + listener.start(); + }); } // fabricate a response in a format usable by ethers users... - async constructUserOpTransactionResponse (userOp1: UserOperationStruct): Promise { - const userOp = await resolveProperties(userOp1) - const userOpHash = getUserOpHash(userOp, this.config.entryPointAddress, this.chainId) - const waitForUserOp = async (): Promise => await new Promise((resolve, reject) => { - new UserOperationEventListener( - resolve, reject, this.entryPoint, userOp.sender, userOpHash, userOp.nonce - ).start() - }) + async constructUserOpTransactionResponse( + userOp1: UserOperationStruct, + ): Promise { + const userOp = await resolveProperties(userOp1); + const userOpHash = getUserOpHash( + userOp, + this.config.entryPointAddress, + this.chainId, + ); + const waitForUserOp = async (): Promise => + await new Promise((resolve, reject) => { + new UserOperationEventListener( + resolve, + reject, + this.entryPoint, + userOp.sender, + userOpHash, + userOp.nonce, + ).start(); + }); return { hash: userOpHash, confirmations: 0, @@ -107,17 +154,17 @@ export class ERC4337EthersProvider extends BaseProvider { data: hexValue(userOp.callData), // should extract the actual called method from this "execFromEntryPoint()" call chainId: this.chainId, wait: async (confirmations?: number): Promise => { - const transactionReceipt = await waitForUserOp() + const transactionReceipt = await waitForUserOp(); if (userOp.initCode.length !== 0) { // checking if the wallet has been deployed by the transaction; it must be if we are here - await this.smartAccountAPI.checkAccountPhantom() + await this.smartAccountAPI.checkAccountPhantom(); } - return transactionReceipt - } - } + return transactionReceipt; + }, + }; } - async detectNetwork (): Promise { - return (this.originalProvider as any).detectNetwork() + async detectNetwork(): Promise { + return (this.originalProvider as any).detectNetwork(); } } diff --git a/src/sdk/ERC4337EthersSigner.ts b/src/sdk/ERC4337EthersSigner.ts index 4653e2e..c5f6a1b 100644 --- a/src/sdk/ERC4337EthersSigner.ts +++ b/src/sdk/ERC4337EthersSigner.ts @@ -1,101 +1,118 @@ -import { Deferrable, defineReadOnly } from '@ethersproject/properties' -import { Provider, TransactionRequest, TransactionResponse } from '@ethersproject/providers' -import { Signer } from '@ethersproject/abstract-signer' +import type { UserOperationStruct } from '@account-abstraction/contracts'; +import { Signer } from '@ethersproject/abstract-signer'; +import type { Deferrable } from '@ethersproject/properties'; +import { defineReadOnly } from '@ethersproject/properties'; +import type { + Provider, + TransactionRequest, + TransactionResponse, +} from '@ethersproject/providers'; +import type { Bytes } from 'ethers'; -import { Bytes } from 'ethers' -import { ERC4337EthersProvider } from './ERC4337EthersProvider' -import { ClientConfig } from './ClientConfig' -import { HttpRpcClient } from './HttpRpcClient' -import { UserOperationStruct } from '@account-abstraction/contracts' -import { BaseAccountAPI } from './BaseAccountAPI' +import type { BaseAccountAPI } from './BaseAccountAPI'; +import type { ClientConfig } from './ClientConfig'; +import type { ERC4337EthersProvider } from './ERC4337EthersProvider'; +import type { HttpRpcClient } from './HttpRpcClient'; export class ERC4337EthersSigner extends Signer { // TODO: we have 'erc4337provider', remove shared dependencies or avoid two-way reference - constructor ( + constructor( readonly config: ClientConfig, readonly originalSigner: Signer, readonly erc4337provider: ERC4337EthersProvider, readonly httpRpcClient: HttpRpcClient, - readonly smartAccountAPI: BaseAccountAPI) { - super() - defineReadOnly(this, 'provider', erc4337provider) + readonly smartAccountAPI: BaseAccountAPI, + ) { + super(); + defineReadOnly(this, 'provider', erc4337provider); } - address?: string + address?: string; // This one is called by Contract. It signs the request and passes in to Provider to be sent. - async sendTransaction (transaction: Deferrable): Promise { - const tx: TransactionRequest = await this.populateTransaction(transaction) - await this.verifyAllNecessaryFields(tx) + async sendTransaction( + transaction: Deferrable, + ): Promise { + const tx: TransactionRequest = await this.populateTransaction(transaction); + await this.verifyAllNecessaryFields(tx); const userOperation = await this.smartAccountAPI.createSignedUserOp({ target: tx.to ?? '', data: tx.data?.toString() ?? '', value: tx.value, - gasLimit: tx.gasLimit - }) - const transactionResponse = await this.erc4337provider.constructUserOpTransactionResponse(userOperation) + gasLimit: tx.gasLimit, + }); + const transactionResponse = + await this.erc4337provider.constructUserOpTransactionResponse( + userOperation, + ); try { - await this.httpRpcClient.sendUserOpToBundler(userOperation) + await this.httpRpcClient.sendUserOpToBundler(userOperation); } catch (error: any) { // console.error('sendUserOpToBundler failed', error) - throw this.unwrapError(error) + throw this.unwrapError(error); } // TODO: handle errors - transaction that is "rejected" by bundler is _not likely_ to ever resolve its "wait()" - return transactionResponse + return transactionResponse; } - unwrapError (errorIn: any): Error { + unwrapError(errorIn: any): Error { if (errorIn.body != null) { - const errorBody = JSON.parse(errorIn.body) - let paymasterInfo: string = '' - let failedOpMessage: string | undefined = errorBody?.error?.message + const errorBody = JSON.parse(errorIn.body); + let paymasterInfo = ''; + let failedOpMessage: string | undefined = errorBody?.error?.message; if (failedOpMessage?.includes('FailedOp') === true) { // TODO: better error extraction methods will be needed - const matched = failedOpMessage.match(/FailedOp\((.*)\)/) + const matched = failedOpMessage.match(/FailedOp\((.*)\)/); if (matched != null) { - const split = matched[1].split(',') - paymasterInfo = `(paymaster address: ${split[1]})` - failedOpMessage = split[2] + const split = matched[1].split(','); + paymasterInfo = `(paymaster address: ${split[1]})`; + failedOpMessage = split[2]; } } - const error = new Error(`The bundler has failed to include UserOperation in a batch: ${failedOpMessage} ${paymasterInfo})`) - error.stack = errorIn.stack - return error + const error = new Error( + `The bundler has failed to include UserOperation in a batch: ${failedOpMessage} ${paymasterInfo})`, + ); + error.stack = errorIn.stack; + return error; } - return errorIn + return errorIn; } - async verifyAllNecessaryFields (transactionRequest: TransactionRequest): Promise { + async verifyAllNecessaryFields( + transactionRequest: TransactionRequest, + ): Promise { if (transactionRequest.to == null) { - throw new Error('Missing call target') + throw new Error('Missing call target'); } if (transactionRequest.data == null && transactionRequest.value == null) { // TBD: banning no-op UserOps seems to make sense on provider level - throw new Error('Missing call data or value') + throw new Error('Missing call data or value'); } } - connect (provider: Provider): Signer { - throw new Error('changing providers is not supported') + connect(provider: Provider): Signer { + throw new Error('changing providers is not supported'); } - async getAddress (): Promise { + async getAddress(): Promise { if (this.address == null) { - this.address = await this.erc4337provider.getSenderAccountAddress() + this.address = await this.erc4337provider.getSenderAccountAddress(); } - return this.address + return this.address; } - async signMessage (message: Bytes | string): Promise { - return await this.originalSigner.signMessage(message) + async signMessage(message: Bytes | string): Promise { + return await this.originalSigner.signMessage(message); } - async signTransaction (transaction: Deferrable): Promise { - throw new Error('not implemented') + async signTransaction( + transaction: Deferrable, + ): Promise { + throw new Error('not implemented'); } - async signUserOperation (userOperation: UserOperationStruct): Promise { - const message = await this.smartAccountAPI.getUserOpHash(userOperation) - return await this.originalSigner.signMessage(message) + async signUserOperation(userOperation: UserOperationStruct): Promise { + const message = await this.smartAccountAPI.getUserOpHash(userOperation); + return await this.originalSigner.signMessage(message); } } diff --git a/src/sdk/HttpRpcClient.ts b/src/sdk/HttpRpcClient.ts index da7b81e..b861b91 100644 --- a/src/sdk/HttpRpcClient.ts +++ b/src/sdk/HttpRpcClient.ts @@ -1,50 +1,61 @@ -import { JsonRpcProvider } from '@ethersproject/providers' -import { ethers } from 'ethers' -import { resolveProperties } from 'ethers/lib/utils' -import { UserOperationStruct } from '@account-abstraction/contracts' -import Debug from 'debug' -import { deepHexlify } from '../utils' +import type { UserOperationStruct } from '@account-abstraction/contracts'; +import type { JsonRpcProvider } from '@ethersproject/providers'; +import Debug from 'debug'; +import { ethers } from 'ethers'; +import { resolveProperties } from 'ethers/lib/utils'; -const debug = Debug('aa.rpc') +import { deepHexlify } from '../utils'; + +const debug = Debug('aa.rpc'); export class HttpRpcClient { - private readonly userOpJsonRpcProvider: JsonRpcProvider + private readonly userOpJsonRpcProvider: JsonRpcProvider; - initializing: Promise + initializing: Promise; - constructor ( + constructor( readonly bundlerUrl: string, readonly entryPointAddress: string, - readonly chainId: number + readonly chainId: number, ) { - this.userOpJsonRpcProvider = new ethers.providers.JsonRpcProvider(this.bundlerUrl, { - name: 'Connected bundler network', - chainId - }) - this.initializing = this.validateChainId() + this.userOpJsonRpcProvider = new ethers.providers.JsonRpcProvider( + this.bundlerUrl, + { + name: 'Connected bundler network', + chainId, + }, + ); + this.initializing = this.validateChainId(); } - async validateChainId (): Promise { + async validateChainId(): Promise { // validate chainId is in sync with expected chainid - const chain = await this.userOpJsonRpcProvider.send('eth_chainId', []) - const bundlerChain = parseInt(chain) + const chain = await this.userOpJsonRpcProvider.send('eth_chainId', []); + const bundlerChain = parseInt(chain); if (bundlerChain !== this.chainId) { - throw new Error(`bundler ${this.bundlerUrl} is on chainId ${bundlerChain}, but provider is on chainId ${this.chainId}`) + throw new Error( + `bundler ${this.bundlerUrl} is on chainId ${bundlerChain}, but provider is on chainId ${this.chainId}`, + ); } } /** * send a UserOperation to the bundler * @param userOp1 - * @return userOpHash the id of this operation, for getUserOperationTransaction + * @returns userOpHash the id of this operation, for getUserOperationTransaction */ - async sendUserOpToBundler (userOp1: UserOperationStruct): Promise { - await this.initializing - const hexifiedUserOp = deepHexlify(await resolveProperties(userOp1)) - const jsonRequestData: [UserOperationStruct, string] = [hexifiedUserOp, this.entryPointAddress] - await this.printUserOperation('eth_sendUserOperation', jsonRequestData) - return await this.userOpJsonRpcProvider - .send('eth_sendUserOperation', [hexifiedUserOp, this.entryPointAddress]) + async sendUserOpToBundler(userOp1: UserOperationStruct): Promise { + await this.initializing; + const hexifiedUserOp = deepHexlify(await resolveProperties(userOp1)); + const jsonRequestData: [UserOperationStruct, string] = [ + hexifiedUserOp, + this.entryPointAddress, + ]; + await this.printUserOperation('eth_sendUserOperation', jsonRequestData); + return await this.userOpJsonRpcProvider.send('eth_sendUserOperation', [ + hexifiedUserOp, + this.entryPointAddress, + ]); } /** @@ -52,21 +63,41 @@ export class HttpRpcClient { * @param userOp1 * @returns latest gas suggestions made by the bundler. */ - async estimateUserOpGas (userOp1: Partial): Promise<{callGasLimit: number, preVerificationGas: number, verificationGasLimit: number}> { - await this.initializing - const hexifiedUserOp = deepHexlify(await resolveProperties(userOp1)) - const jsonRequestData: [UserOperationStruct, string] = [hexifiedUserOp, this.entryPointAddress] - await this.printUserOperation('eth_estimateUserOperationGas', jsonRequestData) - return await this.userOpJsonRpcProvider - .send('eth_estimateUserOperationGas', [hexifiedUserOp, this.entryPointAddress]) + async estimateUserOpGas(userOp1: Partial): Promise<{ + callGasLimit: number; + preVerificationGas: number; + verificationGasLimit: number; + }> { + await this.initializing; + const hexifiedUserOp = deepHexlify(await resolveProperties(userOp1)); + const jsonRequestData: [UserOperationStruct, string] = [ + hexifiedUserOp, + this.entryPointAddress, + ]; + await this.printUserOperation( + 'eth_estimateUserOperationGas', + jsonRequestData, + ); + return await this.userOpJsonRpcProvider.send( + 'eth_estimateUserOperationGas', + [hexifiedUserOp, this.entryPointAddress], + ); } - private async printUserOperation (method: string, [userOp1, entryPointAddress]: [UserOperationStruct, string]): Promise { - const userOp = await resolveProperties(userOp1) - debug('sending', method, { - ...userOp - // initCode: (userOp.initCode ?? '').length, - // callData: (userOp.callData ?? '').length - }, entryPointAddress) + private async printUserOperation( + method: string, + [userOp1, entryPointAddress]: [UserOperationStruct, string], + ): Promise { + const userOp = await resolveProperties(userOp1); + debug( + 'sending', + method, + { + ...userOp, + // initCode: (userOp.initCode ?? '').length, + // callData: (userOp.callData ?? '').length + }, + entryPointAddress, + ); } } diff --git a/src/sdk/PaymasterAPI.ts b/src/sdk/PaymasterAPI.ts index d06c665..134c337 100644 --- a/src/sdk/PaymasterAPI.ts +++ b/src/sdk/PaymasterAPI.ts @@ -1,16 +1,18 @@ -import { UserOperationStruct } from '@account-abstraction/contracts' +import type { UserOperationStruct } from '@account-abstraction/contracts'; /** * an API to external a UserOperation with paymaster info */ export class PaymasterAPI { /** - * @param userOp a partially-filled UserOperation (without signature and paymasterAndData + * @param userOp - a partially-filled UserOperation (without signature and paymasterAndData * note that the "preVerificationGas" is incomplete: it can't account for the * paymasterAndData value, which will only be returned by this method.. * @returns the value to put into the PaymasterAndData, undefined to leave it empty */ - async getPaymasterAndData (userOp: Partial): Promise { - return '0x' + async getPaymasterAndData( + userOp: Partial, + ): Promise { + return '0x'; } } diff --git a/src/sdk/Provider.ts b/src/sdk/Provider.ts index d8acf95..0a6467d 100644 --- a/src/sdk/Provider.ts +++ b/src/sdk/Provider.ts @@ -1,38 +1,53 @@ -import { JsonRpcProvider } from '@ethersproject/providers' +import { + EntryPoint__factory, + SimpleAccountFactory__factory, +} from '@account-abstraction/contracts'; +import type { Signer } from '@ethersproject/abstract-signer'; +import type { JsonRpcProvider } from '@ethersproject/providers'; -import { EntryPoint__factory, SimpleAccountFactory__factory } from '@account-abstraction/contracts' - -import { ClientConfig } from './ClientConfig' -import { SimpleAccountAPI } from './SimpleAccountAPI' -import { ERC4337EthersProvider } from './ERC4337EthersProvider' -import { HttpRpcClient } from './HttpRpcClient' -import { DeterministicDeployer } from './DeterministicDeployer' -import { Signer } from '@ethersproject/abstract-signer' +import type { ClientConfig } from './ClientConfig'; +import { DeterministicDeployer } from './DeterministicDeployer'; +import { ERC4337EthersProvider } from './ERC4337EthersProvider'; +import { HttpRpcClient } from './HttpRpcClient'; +import { SimpleAccountAPI } from './SimpleAccountAPI'; /** * wrap an existing provider to tunnel requests through Account Abstraction. - * @param originalProvider the normal provider - * @param config see ClientConfig for more info - * @param originalSigner use this signer as the owner. of this wallet. By default, use the provider's signer + * @param originalProvider - the normal provider + * @param config - see ClientConfig for more info + * @param originalSigner - use this signer as the owner. of this wallet. By default, use the provider's signer */ -export async function wrapProvider ( +export async function wrapProvider( originalProvider: JsonRpcProvider, config: ClientConfig, - originalSigner: Signer = originalProvider.getSigner() + originalSigner: Signer = originalProvider.getSigner(), ): Promise { - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, originalProvider) + const entryPoint = EntryPoint__factory.connect( + config.entryPointAddress, + originalProvider, + ); // Initial SimpleAccount instance is not deployed and exists just for the interface - const detDeployer = new DeterministicDeployer(originalProvider) - const SimpleAccountFactory = await detDeployer.deterministicDeploy(new SimpleAccountFactory__factory(), 0, [entryPoint.address]) + const detDeployer = new DeterministicDeployer(originalProvider); + const SimpleAccountFactory = await detDeployer.deterministicDeploy( + new SimpleAccountFactory__factory(), + 0, + [entryPoint.address], + ); const smartAccountAPI = new SimpleAccountAPI({ provider: originalProvider, entryPointAddress: entryPoint.address, owner: originalSigner, factoryAddress: SimpleAccountFactory, - paymasterAPI: config.paymasterAPI - }) - const chainId = await originalProvider.getNetwork().then(net => net.chainId) - const httpRpcClient = new HttpRpcClient(config.bundlerUrl, config.entryPointAddress, chainId) + paymasterAPI: config.paymasterAPI, + }); + const chainId = await originalProvider + .getNetwork() + .then((net) => net.chainId); + const httpRpcClient = new HttpRpcClient( + config.bundlerUrl, + config.entryPointAddress, + chainId, + ); return await new ERC4337EthersProvider( chainId, config, @@ -40,6 +55,6 @@ export async function wrapProvider ( originalProvider, httpRpcClient, entryPoint, - smartAccountAPI - ).init() + smartAccountAPI, + ).init(); } diff --git a/src/sdk/SimpleAccountAPI.ts b/src/sdk/SimpleAccountAPI.ts index dd5cc2f..1bdf067 100644 --- a/src/sdk/SimpleAccountAPI.ts +++ b/src/sdk/SimpleAccountAPI.ts @@ -1,26 +1,30 @@ -import { BigNumber, BigNumberish } from 'ethers' -import { +import type { SimpleAccount, - SimpleAccount__factory, SimpleAccountFactory, - SimpleAccountFactory__factory -} from '@account-abstraction/contracts' + SimpleAccountFactory, +} from '@account-abstraction/contracts'; +import { + SimpleAccount__factory, + SimpleAccountFactory__factory, +} from '@account-abstraction/contracts'; +import type { Signer } from '@ethersproject/abstract-signer'; +import type { BigNumberish } from 'ethers'; +import { BigNumber } from 'ethers'; +import { arrayify, hexConcat } from 'ethers/lib/utils'; -import { arrayify, hexConcat } from 'ethers/lib/utils' -import { Signer } from '@ethersproject/abstract-signer' -import { BaseApiParams, BaseAccountAPI } from './BaseAccountAPI' +import type { BaseApiParams } from './BaseAccountAPI'; +import { BaseAccountAPI } from './BaseAccountAPI'; /** * constructor params, added no top of base params: - * @param owner the signer object for the account owner - * @param factoryAddress address of contract "factory" to deploy new contracts (not needed if account already deployed) - * @param index nonce value used when creating multiple accounts for the same owner + * @param owner - the signer object for the account owner + * @param factoryAddress - address of contract "factory" to deploy new contracts (not needed if account already deployed) + * @param index - nonce value used when creating multiple accounts for the same owner */ -export interface SimpleAccountApiParams extends BaseApiParams { - owner: Signer - factoryAddress?: string - index?: BigNumberish - -} +export type SimpleAccountApiParams = { + owner: Signer; + factoryAddress?: string; + index?: BigNumberish; +} & BaseApiParams; /** * An implementation of the BaseAccountAPI using the SimpleAccount contract. @@ -30,56 +34,67 @@ export interface SimpleAccountApiParams extends BaseApiParams { * - execute method is "execFromEntryPoint()" */ export class SimpleAccountAPI extends BaseAccountAPI { - factoryAddress?: string - owner: Signer - index: BigNumberish + factoryAddress?: string; + + owner: Signer; + + index: BigNumberish; /** * our account contract. * should support the "execFromEntryPoint" and "nonce" methods */ - accountContract?: SimpleAccount + accountContract?: SimpleAccount; - factory?: SimpleAccountFactory + factory?: SimpleAccountFactory; - constructor (params: SimpleAccountApiParams) { - super(params) - this.factoryAddress = params.factoryAddress - this.owner = params.owner - this.index = BigNumber.from(params.index ?? 0) + constructor(params: SimpleAccountApiParams) { + super(params); + this.factoryAddress = params.factoryAddress; + this.owner = params.owner; + this.index = BigNumber.from(params.index ?? 0); } - async _getAccountContract (): Promise { + async _getAccountContract(): Promise { if (this.accountContract == null) { - this.accountContract = SimpleAccount__factory.connect(await this.getAccountAddress(), this.provider) + this.accountContract = SimpleAccount__factory.connect( + await this.getAccountAddress(), + this.provider, + ); } - return this.accountContract + return this.accountContract; } /** * return the value to put into the "initCode" field, if the account is not yet deployed. * this value holds the "factory" address, followed by this account's information */ - async getAccountInitCode (): Promise { + async getAccountInitCode(): Promise { if (this.factory == null) { if (this.factoryAddress != null && this.factoryAddress !== '') { - this.factory = SimpleAccountFactory__factory.connect(this.factoryAddress, this.provider) + this.factory = SimpleAccountFactory__factory.connect( + this.factoryAddress, + this.provider, + ); } else { - throw new Error('no factory to get initCode') + throw new Error('no factory to get initCode'); } } return hexConcat([ this.factory.address, - this.factory.interface.encodeFunctionData('createAccount', [await this.owner.getAddress(), this.index]) - ]) + this.factory.interface.encodeFunctionData('createAccount', [ + await this.owner.getAddress(), + this.index, + ]), + ]); } - async getNonce (): Promise { + async getNonce(): Promise { if (await this.checkAccountPhantom()) { - return BigNumber.from(0) + return BigNumber.from(0); } - const accountContract = await this._getAccountContract() - return await accountContract.getNonce() + const accountContract = await this._getAccountContract(); + return await accountContract.getNonce(); } /** @@ -88,18 +103,20 @@ export class SimpleAccountAPI extends BaseAccountAPI { * @param value * @param data */ - async encodeExecute (target: string, value: BigNumberish, data: string): Promise { - const accountContract = await this._getAccountContract() - return accountContract.interface.encodeFunctionData( - 'execute', - [ - target, - value, - data - ]) + async encodeExecute( + target: string, + value: BigNumberish, + data: string, + ): Promise { + const accountContract = await this._getAccountContract(); + return accountContract.interface.encodeFunctionData('execute', [ + target, + value, + data, + ]); } - async signUserOpHash (userOpHash: string): Promise { - return await this.owner.signMessage(arrayify(userOpHash)) + async signUserOpHash(userOpHash: string): Promise { + return await this.owner.signMessage(arrayify(userOpHash)); } } diff --git a/src/sdk/TransactionDetailsForUserOp.ts b/src/sdk/TransactionDetailsForUserOp.ts index 6419f79..7194714 100644 --- a/src/sdk/TransactionDetailsForUserOp.ts +++ b/src/sdk/TransactionDetailsForUserOp.ts @@ -1,11 +1,11 @@ -import { BigNumberish } from 'ethers' +import type { BigNumberish } from 'ethers'; -export interface TransactionDetailsForUserOp { - target: string - data: string - value?: BigNumberish - gasLimit?: BigNumberish - maxFeePerGas?: BigNumberish - maxPriorityFeePerGas?: BigNumberish - nonce?: BigNumberish -} +export type TransactionDetailsForUserOp = { + target: string; + data: string; + value?: BigNumberish; + gasLimit?: BigNumberish; + maxFeePerGas?: BigNumberish; + maxPriorityFeePerGas?: BigNumberish; + nonce?: BigNumberish; +}; diff --git a/src/sdk/UserOperationEventListener.ts b/src/sdk/UserOperationEventListener.ts index 61a2c65..bb93f9f 100644 --- a/src/sdk/UserOperationEventListener.ts +++ b/src/sdk/UserOperationEventListener.ts @@ -1,97 +1,116 @@ -import { BigNumberish, Event } from 'ethers' -import { TransactionReceipt } from '@ethersproject/providers' -import { EntryPoint } from '@account-abstraction/contracts' -import { defaultAbiCoder } from 'ethers/lib/utils' -import Debug from 'debug' +import type { EntryPoint } from '@account-abstraction/contracts'; +import type { TransactionReceipt } from '@ethersproject/providers'; +import Debug from 'debug'; +import type { BigNumberish, Event } from 'ethers'; +import { defaultAbiCoder } from 'ethers/lib/utils'; -const debug = Debug('aa.listener') +const debug = Debug('aa.listener'); -const DEFAULT_TRANSACTION_TIMEOUT: number = 10000 +const DEFAULT_TRANSACTION_TIMEOUT = 10000; /** * This class encapsulates Ethers.js listener function and necessary UserOperation details to * discover a TransactionReceipt for the operation. */ export class UserOperationEventListener { - resolved: boolean = false - boundLisener: (this: any, ...param: any) => void + resolved = false; - constructor ( + boundLisener: (this: any, ...param: any) => void; + + constructor( readonly resolve: (t: TransactionReceipt) => void, readonly reject: (reason?: any) => void, readonly entryPoint: EntryPoint, readonly sender: string, readonly userOpHash: string, readonly nonce?: BigNumberish, - readonly timeout?: number + readonly timeout?: number, ) { // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.boundLisener = this.listenerCallback.bind(this) + this.boundLisener = this.listenerCallback.bind(this); setTimeout(() => { - this.stop() - this.reject(new Error('Timed out')) - }, this.timeout ?? DEFAULT_TRANSACTION_TIMEOUT) + this.stop(); + this.reject(new Error('Timed out')); + }, this.timeout ?? DEFAULT_TRANSACTION_TIMEOUT); } - start (): void { + start(): void { // eslint-disable-next-line @typescript-eslint/no-misused-promises - const filter = this.entryPoint.filters.UserOperationEvent(this.userOpHash) + const filter = this.entryPoint.filters.UserOperationEvent(this.userOpHash); // listener takes time... first query directly: // eslint-disable-next-line @typescript-eslint/no-misused-promises setTimeout(async () => { - const res = await this.entryPoint.queryFilter(filter, 'latest') + const res = await this.entryPoint.queryFilter(filter, 'latest'); if (res.length > 0) { - void this.listenerCallback(res[0]) + void this.listenerCallback(res[0]); } else { - this.entryPoint.once(filter, this.boundLisener) + this.entryPoint.once(filter, this.boundLisener); } - }, 100) + }, 100); } - stop (): void { + stop(): void { // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.entryPoint.off('UserOperationEvent', this.boundLisener) + this.entryPoint.off('UserOperationEvent', this.boundLisener); } - async listenerCallback (this: any, ...param: any): Promise { - const event = arguments[arguments.length - 1] as Event + async listenerCallback(this: any, ...param: any): Promise { + const event = arguments[arguments.length - 1] as Event; if (event.args == null) { - console.error('got event without args', event) - return + console.error('got event without args', event); + return; } // TODO: can this happen? we register to event by userOpHash.. if (event.args.userOpHash !== this.userOpHash) { - console.log(`== event with wrong userOpHash: sender/nonce: event.${event.args.sender as string}@${event.args.nonce.toString() as string}!= userOp.${this.sender as string}@${parseInt(this.nonce?.toString())}`) - return + console.log( + `== event with wrong userOpHash: sender/nonce: event.${ + event.args.sender as string + }@${event.args.nonce.toString() as string}!= userOp.${ + this.sender as string + }@${parseInt(this.nonce?.toString())}`, + ); + return; } - const transactionReceipt = await event.getTransactionReceipt() - transactionReceipt.transactionHash = this.userOpHash - debug('got event with status=', event.args.success, 'gasUsed=', transactionReceipt.gasUsed) + const transactionReceipt = await event.getTransactionReceipt(); + transactionReceipt.transactionHash = this.userOpHash; + debug( + 'got event with status=', + event.args.success, + 'gasUsed=', + transactionReceipt.gasUsed, + ); // before returning the receipt, update the status from the event. // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!event.args.success) { - await this.extractFailureReason(transactionReceipt) + await this.extractFailureReason(transactionReceipt); } - this.stop() - this.resolve(transactionReceipt) - this.resolved = true + this.stop(); + this.resolve(transactionReceipt); + this.resolved = true; } - async extractFailureReason (receipt: TransactionReceipt): Promise { - debug('mark tx as failed') - receipt.status = 0 - const revertReasonEvents = await this.entryPoint.queryFilter(this.entryPoint.filters.UserOperationRevertReason(this.userOpHash, this.sender), receipt.blockHash) + async extractFailureReason(receipt: TransactionReceipt): Promise { + debug('mark tx as failed'); + receipt.status = 0; + const revertReasonEvents = await this.entryPoint.queryFilter( + this.entryPoint.filters.UserOperationRevertReason( + this.userOpHash, + this.sender, + ), + receipt.blockHash, + ); if (revertReasonEvents[0] != null) { - let message = revertReasonEvents[0].args.revertReason + let message = revertReasonEvents[0].args.revertReason; if (message.startsWith('0x08c379a0')) { // Error(string) - message = defaultAbiCoder.decode(['string'], '0x' + message.substring(10)).toString() + message = defaultAbiCoder + .decode(['string'], `0x${message.substring(10)}`) + .toString(); } - debug(`rejecting with reason: ${message}`) - this.reject(new Error(`UserOp failed with reason: ${message}`) - ) + debug(`rejecting with reason: ${message}`); + this.reject(new Error(`UserOp failed with reason: ${message}`)); } } } diff --git a/src/sdk/calcPreVerificationGas.ts b/src/sdk/calcPreVerificationGas.ts index e60701f..6b52d4b 100644 --- a/src/sdk/calcPreVerificationGas.ts +++ b/src/sdk/calcPreVerificationGas.ts @@ -1,45 +1,47 @@ -import { UserOperationStruct } from '@account-abstraction/contracts' -import { NotPromise, packUserOp } from '../utils' -import { arrayify, hexlify } from 'ethers/lib/utils' +import type { UserOperationStruct } from '@account-abstraction/contracts'; +import { arrayify, hexlify } from 'ethers/lib/utils'; -export interface GasOverheads { +import type { NotPromise } from '../utils'; +import { packUserOp } from '../utils'; + +export type GasOverheads = { /** * fixed overhead for entire handleOp bundle. */ - fixed: number + fixed: number; /** * per userOp overhead, added on top of the above fixed per-bundle. */ - perUserOp: number + perUserOp: number; /** * overhead for userOp word (32 bytes) block */ - perUserOpWord: number + perUserOpWord: number; // perCallDataWord: number /** * zero byte cost, for calldata gas cost calculations */ - zeroByte: number + zeroByte: number; /** * non-zero byte cost, for calldata gas cost calculations */ - nonZeroByte: number + nonZeroByte: number; /** * expected bundle size, to split per-bundle overhead between all ops. */ - bundleSize: number + bundleSize: number; /** * expected length of the userOp signature. */ - sigSize: number -} + sigSize: number; +}; export const DefaultGasOverheads: GasOverheads = { fixed: 21000, @@ -48,33 +50,38 @@ export const DefaultGasOverheads: GasOverheads = { zeroByte: 4, nonZeroByte: 16, bundleSize: 1, - sigSize: 65 -} + sigSize: 65, +}; /** * calculate the preVerificationGas of the given UserOperation * preVerificationGas (by definition) is the cost overhead that can't be calculated on-chain. * it is based on parameters that are defined by the Ethereum protocol for external transactions. - * @param userOp filled userOp to calculate. The only possible missing fields can be the signature and preVerificationGas itself - * @param overheads gas overheads to use, to override the default values + * @param userOp - filled userOp to calculate. The only possible missing fields can be the signature and preVerificationGas itself + * @param overheads - gas overheads to use, to override the default values */ -export function calcPreVerificationGas (userOp: Partial>, overheads?: Partial): number { - const ov = { ...DefaultGasOverheads, ...(overheads ?? {}) } +export function calcPreVerificationGas( + userOp: Partial>, + overheads?: Partial, +): number { + const ov = { ...DefaultGasOverheads, ...(overheads ?? {}) }; const p: NotPromise = { // dummy values, in case the UserOp is incomplete. preVerificationGas: 21000, // dummy value, just for calldata cost signature: hexlify(Buffer.alloc(ov.sigSize, 1)), // dummy signature - ...userOp - } as any + ...userOp, + } as any; - const packed = arrayify(packUserOp(p, false)) - const lengthInWord = (packed.length + 31) / 32 - const callDataCost = packed.map(x => x === 0 ? ov.zeroByte : ov.nonZeroByte).reduce((sum, x) => sum + x) + const packed = arrayify(packUserOp(p, false)); + const lengthInWord = (packed.length + 31) / 32; + const callDataCost = packed + .map((x) => (x === 0 ? ov.zeroByte : ov.nonZeroByte)) + .reduce((sum, x) => sum + x); const ret = Math.round( callDataCost + - ov.fixed / ov.bundleSize + - ov.perUserOp + - ov.perUserOpWord * lengthInWord - ) - return ret + ov.fixed / ov.bundleSize + + ov.perUserOp + + ov.perUserOpWord * lengthInWord, + ); + return ret; } diff --git a/src/sdk/index.ts b/src/sdk/index.ts index eabae18..4bcecc3 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -1,10 +1,10 @@ -export { BaseAccountAPI } from './BaseAccountAPI' -export { SimpleAccountAPI } from './SimpleAccountAPI' -export { PaymasterAPI } from './PaymasterAPI' -export { wrapProvider } from './Provider' -export { ERC4337EthersSigner } from './ERC4337EthersSigner' -export { ERC4337EthersProvider } from './ERC4337EthersProvider' -export { ClientConfig } from './ClientConfig' -export { HttpRpcClient } from './HttpRpcClient' -export { DeterministicDeployer } from './DeterministicDeployer' -export * from './calcPreVerificationGas' +export { BaseAccountAPI } from './BaseAccountAPI'; +export { SimpleAccountAPI } from './SimpleAccountAPI'; +export { PaymasterAPI } from './PaymasterAPI'; +export { wrapProvider } from './Provider'; +export { ERC4337EthersSigner } from './ERC4337EthersSigner'; +export { ERC4337EthersProvider } from './ERC4337EthersProvider'; +export type { ClientConfig } from './ClientConfig'; +export { HttpRpcClient } from './HttpRpcClient'; +export { DeterministicDeployer } from './DeterministicDeployer'; +export * from './calcPreVerificationGas'; diff --git a/src/sdk/test/0-deterministicDeployer.test.ts b/src/sdk/test/0-deterministicDeployer.test.ts index 818af2a..e308be7 100644 --- a/src/sdk/test/0-deterministicDeployer.test.ts +++ b/src/sdk/test/0-deterministicDeployer.test.ts @@ -1,26 +1,31 @@ -import { expect } from 'chai' -import { SampleRecipient__factory } from '../../contract-types' -import { ethers } from 'hardhat' -import { hexValue } from 'ethers/lib/utils' -import { DeterministicDeployer } from '../DeterministicDeployer' +import { expect } from 'chai'; +import { hexValue } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; -const deployer = new DeterministicDeployer(ethers.provider) +import { SampleRecipient__factory } from '../../contract-types'; +import { DeterministicDeployer } from '../DeterministicDeployer'; + +const deployer = new DeterministicDeployer(ethers.provider); describe('#deterministicDeployer', () => { it('deploy deployer', async () => { - expect(await deployer.isDeployerDeployed()).to.equal(false) - await deployer.deployFactory() - expect(await deployer.isDeployerDeployed()).to.equal(true) - }) + expect(await deployer.isDeployerDeployed()).to.equal(false); + await deployer.deployFactory(); + expect(await deployer.isDeployerDeployed()).to.equal(true); + }); it('should ignore deploy again of deployer', async () => { - await deployer.deployFactory() - }) + await deployer.deployFactory(); + }); it('should deploy at given address', async () => { - const ctr = hexValue(new SampleRecipient__factory(ethers.provider.getSigner()).getDeployTransaction().data!) - DeterministicDeployer.init(ethers.provider) - const addr = await DeterministicDeployer.getAddress(ctr) - expect(await deployer.isContractDeployed(addr)).to.equal(false) - await DeterministicDeployer.deploy(ctr) - expect(await deployer.isContractDeployed(addr)).to.equal(true) - }) -}) + const ctr = hexValue( + new SampleRecipient__factory( + ethers.provider.getSigner(), + ).getDeployTransaction().data!, + ); + DeterministicDeployer.init(ethers.provider); + const addr = await DeterministicDeployer.getAddress(ctr); + expect(await deployer.isContractDeployed(addr)).to.equal(false); + await DeterministicDeployer.deploy(ctr); + expect(await deployer.isContractDeployed(addr)).to.equal(true); + }); +}); diff --git a/src/sdk/test/1-SimpleAccountAPI.test.ts b/src/sdk/test/1-SimpleAccountAPI.test.ts index 3c64e04..eeddf81 100644 --- a/src/sdk/test/1-SimpleAccountAPI.test.ts +++ b/src/sdk/test/1-SimpleAccountAPI.test.ts @@ -1,45 +1,53 @@ -import { +import type { EntryPoint, + UserOperationStruct, +} from '@account-abstraction/contracts'; +import { EntryPoint__factory, SimpleAccountFactory__factory, - UserOperationStruct -} from '@account-abstraction/contracts' -import { Wallet } from 'ethers' -import { parseEther } from 'ethers/lib/utils' -import { expect } from 'chai' -import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' -import { ethers } from 'hardhat' -import { DeterministicDeployer, SimpleAccountAPI } from '..' -import { SampleRecipient, SampleRecipient__factory } from '../../contract-types' -import { rethrowError } from '../../utils' +} from '@account-abstraction/contracts'; +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; +import { expect } from 'chai'; +import { Wallet } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; -const provider = ethers.provider -const signer = provider.getSigner() +import { DeterministicDeployer, SimpleAccountAPI } from '..'; +import type { SampleRecipient } from '../../contract-types'; +import { SampleRecipient__factory } from '../../contract-types'; +import { rethrowError } from '../../utils'; + +const { provider } = ethers; +const signer = provider.getSigner(); describe('SimpleAccountAPI', () => { - let owner: Wallet - let api: SimpleAccountAPI - let entryPoint: EntryPoint - let beneficiary: string - let recipient: SampleRecipient - let accountAddress: string - let accountDeployed = false + let owner: Wallet; + let api: SimpleAccountAPI; + let entryPoint: EntryPoint; + let beneficiary: string; + let recipient: SampleRecipient; + let accountAddress: string; + let accountDeployed = false; before('init', async () => { - entryPoint = await new EntryPoint__factory(signer).deploy() - beneficiary = await signer.getAddress() + entryPoint = await new EntryPoint__factory(signer).deploy(); + beneficiary = await signer.getAddress(); - recipient = await new SampleRecipient__factory(signer).deploy() - owner = Wallet.createRandom() - DeterministicDeployer.init(ethers.provider) - const factoryAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [entryPoint.address]) + recipient = await new SampleRecipient__factory(signer).deploy(); + owner = Wallet.createRandom(); + DeterministicDeployer.init(ethers.provider); + const factoryAddress = await DeterministicDeployer.deploy( + new SimpleAccountFactory__factory(), + 0, + [entryPoint.address], + ); api = new SimpleAccountAPI({ provider, entryPointAddress: entryPoint.address, owner, - factoryAddress - }) - }) + factoryAddress, + }); + }); it('#getUserOpHash should match entryPoint.getUserOpHash', async function () { const userOp: UserOperationStruct = { @@ -53,77 +61,82 @@ describe('SimpleAccountAPI', () => { maxFeePerGas: 8, maxPriorityFeePerGas: 9, paymasterAndData: '0xaaaaaa', - signature: '0xbbbb' - } - const hash = await api.getUserOpHash(userOp) - const epHash = await entryPoint.getUserOpHash(userOp) - expect(hash).to.equal(epHash) - }) + signature: '0xbbbb', + }; + const hash = await api.getUserOpHash(userOp); + const epHash = await entryPoint.getUserOpHash(userOp); + expect(hash).to.equal(epHash); + }); it('should deploy to counterfactual address', async () => { - accountAddress = await api.getAccountAddress() - expect(await provider.getCode(accountAddress).then(code => code.length)).to.equal(2) + accountAddress = await api.getAccountAddress(); + expect( + await provider.getCode(accountAddress).then((code) => code.length), + ).to.equal(2); await signer.sendTransaction({ to: accountAddress, - value: parseEther('0.1') - }) + value: parseEther('0.1'), + }); const op = await api.createSignedUserOp({ target: recipient.address, - data: recipient.interface.encodeFunctionData('something', ['hello']) - }) + data: recipient.interface.encodeFunctionData('something', ['hello']), + }); - await expect(entryPoint.handleOps([op], beneficiary)).to.emit(recipient, 'Sender') - .withArgs(anyValue, accountAddress, 'hello') - expect(await provider.getCode(accountAddress).then(code => code.length)).to.greaterThan(1000) - accountDeployed = true - }) + await expect(entryPoint.handleOps([op], beneficiary)) + .to.emit(recipient, 'Sender') + .withArgs(anyValue, accountAddress, 'hello'); + expect( + await provider.getCode(accountAddress).then((code) => code.length), + ).to.greaterThan(1000); + accountDeployed = true; + }); context('#rethrowError', () => { - let userOp: UserOperationStruct + let userOp: UserOperationStruct; before(async () => { userOp = await api.createUnsignedUserOp({ target: ethers.constants.AddressZero, - data: '0x' - }) + data: '0x', + }); // expect FailedOp "invalid signature length" - userOp.signature = '0x11' - }) + userOp.signature = '0x11'; + }); it('should parse FailedOp error', async () => { await expect( - entryPoint.handleOps([userOp], beneficiary) - .catch(rethrowError)) - .to.revertedWith('FailedOp: AA23 reverted: ECDSA: invalid signature length') - }) + entryPoint.handleOps([userOp], beneficiary).catch(rethrowError), + ).to.revertedWith( + 'FailedOp: AA23 reverted: ECDSA: invalid signature length', + ); + }); it('should parse Error(message) error', async () => { - await expect( - entryPoint.addStake(0) - ).to.revertedWith('must specify unstake delay') - }) + await expect(entryPoint.addStake(0)).to.revertedWith( + 'must specify unstake delay', + ); + }); it('should parse revert with no description', async () => { // use wrong signature for contract.. - const wrongContract = entryPoint.attach(recipient.address) - await expect( - wrongContract.addStake(0) - ).to.revertedWithoutReason() - }) - }) + const wrongContract = entryPoint.attach(recipient.address); + await expect(wrongContract.addStake(0)).to.revertedWithoutReason(); + }); + }); it('should use account API after creation without a factory', async function () { if (!accountDeployed) { - this.skip() + this.skip(); } const api1 = new SimpleAccountAPI({ provider, entryPointAddress: entryPoint.address, accountAddress, - owner - }) + owner, + }); const op1 = await api1.createSignedUserOp({ target: recipient.address, - data: recipient.interface.encodeFunctionData('something', ['world']) - }) - await expect(entryPoint.handleOps([op1], beneficiary)).to.emit(recipient, 'Sender') - .withArgs(anyValue, accountAddress, 'world') - }) -}) + data: recipient.interface.encodeFunctionData('something', ['world']), + }); + await expect(entryPoint.handleOps([op1], beneficiary)) + .to.emit(recipient, 'Sender') + .withArgs(anyValue, accountAddress, 'world'); + }); +}); diff --git a/src/sdk/test/3-ERC4337EthersSigner.test.ts b/src/sdk/test/3-ERC4337EthersSigner.test.ts index 2cb61b1..f80572a 100644 --- a/src/sdk/test/3-ERC4337EthersSigner.test.ts +++ b/src/sdk/test/3-ERC4337EthersSigner.test.ts @@ -1,76 +1,83 @@ -import { SampleRecipient, SampleRecipient__factory } from '../../contract-types' -import { ethers } from 'hardhat' -import { ClientConfig, ERC4337EthersProvider, wrapProvider } from '..' -import { EntryPoint, EntryPoint__factory } from '@account-abstraction/contracts' -import { expect } from 'chai' -import { parseEther } from 'ethers/lib/utils' -import { Wallet } from 'ethers' -import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' +import type { EntryPoint } from '@account-abstraction/contracts'; +import { EntryPoint__factory } from '@account-abstraction/contracts'; +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; +import { expect } from 'chai'; +import { Wallet } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; -const provider = ethers.provider -const signer = provider.getSigner() +import { wrapProvider } from '..'; +import type { ClientConfig, ERC4337EthersProvider } from '..'; +import { SampleRecipient__factory } from '../../contract-types'; +import type { SampleRecipient } from '../../contract-types'; + +const { provider } = ethers; +const signer = provider.getSigner(); describe('ERC4337EthersSigner, Provider', function () { - let recipient: SampleRecipient - let aaProvider: ERC4337EthersProvider - let entryPoint: EntryPoint + let recipient: SampleRecipient; + let aaProvider: ERC4337EthersProvider; + let entryPoint: EntryPoint; before('init', async () => { - const deployRecipient = await new SampleRecipient__factory(signer).deploy() - entryPoint = await new EntryPoint__factory(signer).deploy() + const deployRecipient = await new SampleRecipient__factory(signer).deploy(); + entryPoint = await new EntryPoint__factory(signer).deploy(); const config: ClientConfig = { entryPointAddress: entryPoint.address, - bundlerUrl: '' - } - const aasigner = Wallet.createRandom() - aaProvider = await wrapProvider(provider, config, aasigner) + bundlerUrl: '', + }; + const aasigner = Wallet.createRandom(); + aaProvider = await wrapProvider(provider, config, aasigner); - const beneficiary = provider.getSigner().getAddress() + const beneficiary = provider.getSigner().getAddress(); // for testing: bypass sending through a bundler, and send directly to our entrypoint.. aaProvider.httpRpcClient.sendUserOpToBundler = async (userOp) => { try { - await entryPoint.handleOps([userOp], beneficiary) + await entryPoint.handleOps([userOp], beneficiary); } catch (e: any) { // doesn't report error unless called with callStatic - await entryPoint.callStatic.handleOps([userOp], beneficiary).catch((e: any) => { - // eslint-disable-next-line + await entryPoint.callStatic + .handleOps([userOp], beneficiary) + .catch((e: any) => { + // eslint-disable-next-line const message = e.errorArgs != null ? `${e.errorName}(${e.errorArgs.join(',')})` : e.message - throw new Error(message) - }) + throw new Error(message); + }); } - return '' - } - recipient = deployRecipient.connect(aaProvider.getSigner()) - }) + return ''; + }; + recipient = deployRecipient.connect(aaProvider.getSigner()); + }); it('should fail to send before funding', async () => { try { - await recipient.something('hello', { gasLimit: 1e6 }) - throw new Error('should revert') + await recipient.something('hello', { gasLimit: 1e6 }); + throw new Error('should revert'); } catch (e: any) { - expect(e.message).to.eq('FailedOp(0,AA21 didn\'t pay prefund)') + expect(e.message).to.eq("FailedOp(0,AA21 didn't pay prefund)"); } - }) + }); it('should use ERC-4337 Signer and Provider to send the UserOperation to the bundler', async function () { - const accountAddress = await aaProvider.getSigner().getAddress() + const accountAddress = await aaProvider.getSigner().getAddress(); await signer.sendTransaction({ to: accountAddress, - value: parseEther('0.1') - }) - const ret = await recipient.something('hello') - await expect(ret).to.emit(recipient, 'Sender') - .withArgs(anyValue, accountAddress, 'hello') - }) + value: parseEther('0.1'), + }); + const ret = await recipient.something('hello'); + await expect(ret) + .to.emit(recipient, 'Sender') + .withArgs(anyValue, accountAddress, 'hello'); + }); it('should revert if on-chain userOp execution reverts', async function () { // specifying gas, so that estimateGas won't revert.. - const ret = await recipient.reverting({ gasLimit: 10000 }) + const ret = await recipient.reverting({ gasLimit: 10000 }); try { - await ret.wait() - throw new Error('expected to revert') + await ret.wait(); + throw new Error('expected to revert'); } catch (e: any) { - expect(e.message).to.match(/test revert/) + expect(e.message).to.match(/test revert/); } - }) -}) + }); +}); diff --git a/src/sdk/test/4-calcPreVerificationGas.test.ts b/src/sdk/test/4-calcPreVerificationGas.test.ts index e74f217..7c9bcd9 100644 --- a/src/sdk/test/4-calcPreVerificationGas.test.ts +++ b/src/sdk/test/4-calcPreVerificationGas.test.ts @@ -1,6 +1,7 @@ -import { expect } from 'chai' -import { hexlify } from 'ethers/lib/utils' -import { calcPreVerificationGas } from '../calcPreVerificationGas' +import { expect } from 'chai'; +import { hexlify } from 'ethers/lib/utils'; + +import { calcPreVerificationGas } from '../calcPreVerificationGas'; describe('#calcPreVerificationGas', () => { const userOp = { @@ -12,24 +13,24 @@ describe('#calcPreVerificationGas', () => { verificationGasLimit: 6, maxFeePerGas: 8, maxPriorityFeePerGas: 9, - paymasterAndData: '0xaaaaaa' - } + paymasterAndData: '0xaaaaaa', + }; it('returns a gas value proportional to sigSize', async () => { - const pvg1 = calcPreVerificationGas(userOp, { sigSize: 0 }) - const pvg2 = calcPreVerificationGas(userOp, { sigSize: 65 }) + const pvg1 = calcPreVerificationGas(userOp, { sigSize: 0 }); + const pvg2 = calcPreVerificationGas(userOp, { sigSize: 65 }); - expect(pvg2).to.be.greaterThan(pvg1) - }) + expect(pvg2).to.be.greaterThan(pvg1); + }); it('returns a gas value that ignores sigSize if userOp already signed', async () => { const userOpWithSig = { ...userOp, - signature: hexlify(Buffer.alloc(65, 1)) - } + signature: hexlify(Buffer.alloc(65, 1)), + }; - const pvg1 = calcPreVerificationGas(userOpWithSig, { sigSize: 0 }) - const pvg2 = calcPreVerificationGas(userOpWithSig) - expect(pvg2).to.equal(pvg1) - }) -}) + const pvg1 = calcPreVerificationGas(userOpWithSig, { sigSize: 0 }); + const pvg2 = calcPreVerificationGas(userOpWithSig); + expect(pvg2).to.equal(pvg1); + }); +}); diff --git a/src/utils/ERC4337Utils.ts b/src/utils/ERC4337Utils.ts index 966afd7..f1c47e7 100644 --- a/src/utils/ERC4337Utils.ts +++ b/src/utils/ERC4337Utils.ts @@ -1,50 +1,105 @@ -import { defaultAbiCoder, hexConcat, hexlify, keccak256, resolveProperties } from 'ethers/lib/utils' -import { UserOperationStruct } from '@account-abstraction/contracts' -import { abi as entryPointAbi } from '@account-abstraction/contracts/artifacts/IEntryPoint.json' -import { ethers } from 'ethers' -import Debug from 'debug' +import type { UserOperationStruct } from '@account-abstraction/contracts'; +import { abi as entryPointAbi } from '@account-abstraction/contracts/artifacts/IEntryPoint.json'; +import Debug from 'debug'; +import { ethers } from 'ethers'; +import { + defaultAbiCoder, + hexConcat, + hexlify, + keccak256, + resolveProperties, +} from 'ethers/lib/utils'; -const debug = Debug('aa.utils') +const debug = Debug('aa.utils'); // UserOperation is the first parameter of validateUseOp -const validateUserOpMethod = 'simulateValidation' -const UserOpType = entryPointAbi.find(entry => entry.name === validateUserOpMethod)?.inputs[0] +const validateUserOpMethod = 'simulateValidation'; +const UserOpType = entryPointAbi.find( + (entry) => entry.name === validateUserOpMethod, +)?.inputs[0]; if (UserOpType == null) { - throw new Error(`unable to find method ${validateUserOpMethod} in EP ${entryPointAbi.filter(x => x.type === 'function').map(x => x.name).join(',')}`) + throw new Error( + `unable to find method ${validateUserOpMethod} in EP ${entryPointAbi + .filter((x) => x.type === 'function') + .map((x) => x.name) + .join(',')}`, + ); } -export const AddressZero = ethers.constants.AddressZero +export const { AddressZero } = ethers.constants; // reverse "Deferrable" or "PromiseOrValue" fields export type NotPromise = { - [P in keyof T]: Exclude> -} + [P in keyof T]: Exclude>; +}; /** * pack the userOperation * @param op - * @param forSignature "true" if the hash is needed to calculate the getUserOpHash() + * @param forSignature - "true" if the hash is needed to calculate the getUserOpHash() * "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain. */ -export function packUserOp (op: NotPromise, forSignature = true): string { +export function packUserOp( + op: NotPromise, + forSignature = true, +): string { if (forSignature) { return defaultAbiCoder.encode( - ['address', 'uint256', 'bytes32', 'bytes32', - 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', - 'bytes32'], - [op.sender, op.nonce, keccak256(op.initCode), keccak256(op.callData), - op.callGasLimit, op.verificationGasLimit, op.preVerificationGas, op.maxFeePerGas, op.maxPriorityFeePerGas, - keccak256(op.paymasterAndData)]) - } else { - // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) - return defaultAbiCoder.encode( - ['address', 'uint256', 'bytes', 'bytes', - 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', - 'bytes', 'bytes'], - [op.sender, op.nonce, op.initCode, op.callData, - op.callGasLimit, op.verificationGasLimit, op.preVerificationGas, op.maxFeePerGas, op.maxPriorityFeePerGas, - op.paymasterAndData, op.signature]) + [ + 'address', + 'uint256', + 'bytes32', + 'bytes32', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'bytes32', + ], + [ + op.sender, + op.nonce, + keccak256(op.initCode), + keccak256(op.callData), + op.callGasLimit, + op.verificationGasLimit, + op.preVerificationGas, + op.maxFeePerGas, + op.maxPriorityFeePerGas, + keccak256(op.paymasterAndData), + ], + ); } + // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) + return defaultAbiCoder.encode( + [ + 'address', + 'uint256', + 'bytes', + 'bytes', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'bytes', + 'bytes', + ], + [ + op.sender, + op.nonce, + op.initCode, + op.callData, + op.callGasLimit, + op.verificationGasLimit, + op.preVerificationGas, + op.maxFeePerGas, + op.maxPriorityFeePerGas, + op.paymasterAndData, + op.signature, + ], + ); } /** @@ -56,37 +111,52 @@ export function packUserOp (op: NotPromise, forSignature = * @param entryPoint * @param chainId */ -export function getUserOpHash (op: NotPromise, entryPoint: string, chainId: number): string { - const userOpHash = keccak256(packUserOp(op, true)) +export function getUserOpHash( + op: NotPromise, + entryPoint: string, + chainId: number, +): string { + const userOpHash = keccak256(packUserOp(op, true)); const enc = defaultAbiCoder.encode( ['bytes32', 'address', 'uint256'], - [userOpHash, entryPoint, chainId]) - return keccak256(enc) + [userOpHash, entryPoint, chainId], + ); + return keccak256(enc); } -const ErrorSig = keccak256(Buffer.from('Error(string)')).slice(0, 10) // 0x08c379a0 -const FailedOpSig = keccak256(Buffer.from('FailedOp(uint256,string)')).slice(0, 10) // 0x220266b6 +const ErrorSig = keccak256(Buffer.from('Error(string)')).slice(0, 10); // 0x08c379a0 +const FailedOpSig = keccak256(Buffer.from('FailedOp(uint256,string)')).slice( + 0, + 10, +); // 0x220266b6 -interface DecodedError { - message: string - opIndex?: number -} +type DecodedError = { + message: string; + opIndex?: number; +}; /** * decode bytes thrown by revert as Error(message) or FailedOp(opIndex,paymaster,message) + * @param error */ -export function decodeErrorReason (error: string): DecodedError | undefined { - debug('decoding', error) +export function decodeErrorReason(error: string): DecodedError | undefined { + debug('decoding', error); if (error.startsWith(ErrorSig)) { - const [message] = defaultAbiCoder.decode(['string'], '0x' + error.substring(10)) - return { message } + const [message] = defaultAbiCoder.decode( + ['string'], + `0x${error.substring(10)}`, + ); + return { message }; } else if (error.startsWith(FailedOpSig)) { - let [opIndex, message] = defaultAbiCoder.decode(['uint256', 'string'], '0x' + error.substring(10)) - message = `FailedOp: ${message as string}` + let [opIndex, message] = defaultAbiCoder.decode( + ['uint256', 'string'], + `0x${error.substring(10)}`, + ); + message = `FailedOp: ${message as string}`; return { message, - opIndex - } + opIndex, + }; } } @@ -95,56 +165,69 @@ export function decodeErrorReason (error: string): DecodedError | undefined { * updated both "message" and inner encoded "data" * tested on geth, hardhat-node * usage: entryPoint.handleOps().catch(decodeError) + * @param e */ -export function rethrowError (e: any): any { - let error = e - let parent = e +export function rethrowError(e: any): any { + let error = e; + let parent = e; if (error?.error != null) { - error = error.error + error = error.error; } while (error?.data != null) { - parent = error - error = error.data + parent = error; + error = error.data; } - const decoded = typeof error === 'string' && error.length > 2 ? decodeErrorReason(error) : undefined + const decoded = + typeof error === 'string' && error.length > 2 + ? decodeErrorReason(error) + : undefined; if (decoded != null) { - e.message = decoded.message + e.message = decoded.message; if (decoded.opIndex != null) { // helper for chai: convert our FailedOp error into "Error(msg)" - const errorWithMsg = hexConcat([ErrorSig, defaultAbiCoder.encode(['string'], [decoded.message])]) + const errorWithMsg = hexConcat([ + ErrorSig, + defaultAbiCoder.encode(['string'], [decoded.message]), + ]); // modify in-place the error object: - parent.data = errorWithMsg + parent.data = errorWithMsg; } } - throw e + throw e; } /** * hexlify all members of object, recursively * @param obj */ -export function deepHexlify (obj: any): any { +export function deepHexlify(obj: any): any { if (typeof obj === 'function') { - return undefined + return undefined; } if (obj == null || typeof obj === 'string' || typeof obj === 'boolean') { - return obj + return obj; } else if (obj._isBigNumber != null || typeof obj !== 'object') { - return hexlify(obj).replace(/^0x0/, '0x') + return hexlify(obj).replace(/^0x0/, '0x'); } if (Array.isArray(obj)) { - return obj.map(member => deepHexlify(member)) + return obj.map((member) => deepHexlify(member)); } - return Object.keys(obj) - .reduce((set, key) => ({ + return Object.keys(obj).reduce( + (set, key) => ({ ...set, - [key]: deepHexlify(obj[key]) - }), {}) + [key]: deepHexlify(obj[key]), + }), + {}, + ); } // resolve all property and hexlify. // (UserOpMethodHandler receives data from the network, so we need to pack our generated values) -export async function resolveHexlify (a: any): Promise { - return deepHexlify(await resolveProperties(a)) +/** + * + * @param a + */ +export async function resolveHexlify(a: any): Promise { + return deepHexlify(await resolveProperties(a)); } diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index f838a7b..a9f6566 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -1,31 +1,34 @@ // misc utilities for the various modules. -import { BytesLike, ContractFactory, BigNumber } from 'ethers' -import { hexlify, hexZeroPad, Result } from 'ethers/lib/utils' -import { Provider, JsonRpcProvider } from '@ethersproject/providers' -import { BigNumberish } from 'ethers/lib/ethers' -import { NotPromise } from './ERC4337Utils' -import { UserOperationStruct } from '@account-abstraction/contracts' - -export interface SlotMap { - [slot: string]: string -} +import type { UserOperationStruct } from '@account-abstraction/contracts'; +import type { Provider, JsonRpcProvider } from '@ethersproject/providers'; +import type { BytesLike } from 'ethers'; +import { ContractFactory, BigNumber } from 'ethers'; +import type { BigNumberish } from 'ethers/lib/ethers'; +import type { Result } from 'ethers/lib/utils'; +import { hexlify, hexZeroPad } from 'ethers/lib/utils'; + +import type { NotPromise } from './ERC4337Utils'; + +export type SlotMap = { + [slot: string]: string; +}; /** * map of storage * for each address, either a root hash, or a map of slot:value */ -export interface StorageMap { - [address: string]: string | SlotMap -} +export type StorageMap = { + [address: string]: string | SlotMap; +}; -export interface StakeInfo { - addr: string - stake: BigNumberish - unstakeDelaySec: BigNumberish -} +export type StakeInfo = { + addr: string; + stake: BigNumberish; + unstakeDelaySec: BigNumberish; +}; -export type UserOperation = NotPromise +export type UserOperation = NotPromise; export enum ValidationErrors { InvalidFields = -32602, @@ -37,86 +40,138 @@ export enum ValidationErrors { InsufficientStake = -32505, UnsupportedSignatureAggregator = -32506, InvalidSignature = -32507, - UserOperationReverted = -32521 + UserOperationReverted = -32521, } -export interface ReferencedCodeHashes { +export type ReferencedCodeHashes = { // addresses accessed during this user operation - addresses: string[] + addresses: string[]; // keccak over the code of all referenced addresses - hash: string -} + hash: string; +}; export class RpcError extends Error { // error codes from: https://eips.ethereum.org/EIPS/eip-1474 - constructor (msg: string, readonly code?: number, readonly data: any = undefined) { - super(msg) + constructor( + msg: string, + readonly code?: number, + readonly data: any = undefined, + ) { + super(msg); } } -export function tostr (s: BigNumberish): string { - return BigNumber.from(s).toString() +/** + * + * @param s + */ +export function tostr(s: BigNumberish): string { + return BigNumber.from(s).toString(); } -export function requireCond (cond: boolean, msg: string, code?: number, data: any = undefined): void { +/** + * + * @param cond + * @param msg + * @param code + * @param data + */ +export function requireCond( + cond: boolean, + msg: string, + code?: number, + data: any = undefined, +): void { if (!cond) { - throw new RpcError(msg, code, data) + throw new RpcError(msg, code, data); } } /** * create a dictionary object with given keys - * @param keys the property names of the returned object - * @param mapper mapper from key to property value - * @param filter if exists, must return true to add keys + * @param keys - the property names of the returned object + * @param mapper - mapper from key to property value + * @param filter - if exists, must return true to add keys */ -export function mapOf (keys: Iterable, mapper: (key: string) => T, filter?: (key: string) => boolean): { - [key: string]: T +export function mapOf( + keys: Iterable, + mapper: (key: string) => T, + filter?: (key: string) => boolean, +): { + [key: string]: T; } { - const ret: { [key: string]: T } = {} + const ret: { [key: string]: T } = {}; for (const key of keys) { if (filter == null || filter(key)) { - ret[key] = mapper(key) + ret[key] = mapper(key); } } - return ret + return ret; } -export async function sleep (sleepTime: number): Promise { - await new Promise(resolve => setTimeout(resolve, sleepTime)) +/** + * + * @param sleepTime + */ +export async function sleep(sleepTime: number): Promise { + await new Promise((resolve) => setTimeout(resolve, sleepTime)); } -export async function waitFor (func: () => T | undefined, timeout = 10000, interval = 500): Promise { - const endTime = Date.now() + timeout +/** + * + * @param func + * @param timeout + * @param interval + */ +export async function waitFor( + func: () => T | undefined, + timeout = 10000, + interval = 500, +): Promise { + const endTime = Date.now() + timeout; while (true) { - const ret = await func() + const ret = await func(); if (ret != null) { - return ret + return ret; } if (Date.now() > endTime) { - throw new Error(`Timed out waiting for ${func as unknown as string}`) + throw new Error(`Timed out waiting for ${func as unknown as string}`); } - await sleep(interval) + await sleep(interval); } } -export async function supportsRpcMethod (provider: JsonRpcProvider, method: string, params: any[]): Promise { - const ret = await provider.send(method, params).catch(e => e) - const code = ret.error?.code ?? ret.code - return code === -32602 // wrong params (meaning, method exists) +/** + * + * @param provider + * @param method + * @param params + */ +export async function supportsRpcMethod( + provider: JsonRpcProvider, + method: string, + params: any[], +): Promise { + const ret = await provider.send(method, params).catch((e) => e); + const code = ret.error?.code ?? ret.code; + return code === -32602; // wrong params (meaning, method exists) } // extract address from initCode or paymasterAndData -export function getAddr (data?: BytesLike): string | undefined { +/** + * + * @param data + */ +export function getAddr(data?: BytesLike): string | undefined { if (data == null) { - return undefined + return undefined; } - const str = hexlify(data) + const str = hexlify(data); if (str.length >= 42) { - return str.slice(0, 42) + return str.slice(0, 42); } - return undefined + return undefined; } /** @@ -128,46 +183,59 @@ export function getAddr (data?: BytesLike): string | undefined { * @param mergedStorageMap * @param validationStorageMap */ -export function mergeStorageMap (mergedStorageMap: StorageMap, validationStorageMap: StorageMap): StorageMap { +export function mergeStorageMap( + mergedStorageMap: StorageMap, + validationStorageMap: StorageMap, +): StorageMap { Object.entries(validationStorageMap).forEach(([addr, validationEntry]) => { if (typeof validationEntry === 'string') { // it's a root. override specific slots, if any - mergedStorageMap[addr] = validationEntry + mergedStorageMap[addr] = validationEntry; } else if (typeof mergedStorageMap[addr] === 'string') { // merged address already contains a root. ignore specific slot values } else { - let slots: SlotMap + let slots: SlotMap; if (mergedStorageMap[addr] == null) { - slots = mergedStorageMap[addr] = {} + slots = mergedStorageMap[addr] = {}; } else { - slots = mergedStorageMap[addr] as SlotMap + slots = mergedStorageMap[addr] as SlotMap; } Object.entries(validationEntry).forEach(([slot, val]) => { - slots[slot] = val - }) + slots[slot] = val; + }); } - }) - return mergedStorageMap + }); + return mergedStorageMap; } -export function toBytes32 (b: BytesLike | number): string { - return hexZeroPad(hexlify(b).toLowerCase(), 32) +/** + * + * @param b + */ +export function toBytes32(b: BytesLike | number): string { + return hexZeroPad(hexlify(b).toLowerCase(), 32); } /** * run the constructor of the given type as a script: it is expected to revert with the script's return values. - * @param provider provider to use fo rthe call + * @param provider - provider to use fo rthe call * @param c - contract factory of the script class - * @param ctrParams constructor parameters - * @return an array of arguments of the error + * @param ctrParams - constructor parameters + * @returns an array of arguments of the error * example usasge: * hashes = await runContractScript(provider, new GetUserOpHashes__factory(), [entryPoint.address, userOps]).then(ret => ret.userOpHashes) */ -export async function runContractScript (provider: Provider, c: T, ctrParams: Parameters): Promise { - const tx = c.getDeployTransaction(...ctrParams) - const ret = await provider.call(tx) - const parsed = ContractFactory.getInterface(c.interface).parseError(ret) - if (parsed == null) throw new Error('unable to parse script (error) response: ' + ret) - return parsed.args +export async function runContractScript( + provider: Provider, + c: T, + ctrParams: Parameters, +): Promise { + const tx = c.getDeployTransaction(...ctrParams); + const ret = await provider.call(tx); + const parsed = ContractFactory.getInterface(c.interface).parseError(ret); + if (parsed == null) { + throw new Error(`unable to parse script (error) response: ${ret}`); + } + return parsed.args; } diff --git a/src/utils/Version.ts b/src/utils/Version.ts index 51aa3dc..0af8200 100644 --- a/src/utils/Version.ts +++ b/src/utils/Version.ts @@ -1,2 +1,3 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires -export const erc4337RuntimeVersion: string = require('../../package.json').version +export const erc4337RuntimeVersion: string = + require('../../package.json').version; diff --git a/src/utils/index.ts b/src/utils/index.ts index 3ce45c3..08345b9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,3 @@ -export * from './Version' -export * from './ERC4337Utils' -export * from './Utils' +export * from './Version'; +export * from './ERC4337Utils'; +export * from './Utils'; diff --git a/src/utils/postExecCheck.ts b/src/utils/postExecCheck.ts index 0b9af89..f4f90a8 100644 --- a/src/utils/postExecCheck.ts +++ b/src/utils/postExecCheck.ts @@ -1,15 +1,41 @@ -import { resolveProperties } from 'ethers/lib/utils' -import { NotPromise } from './ERC4337Utils' -import { EntryPoint, UserOperationStruct } from '@account-abstraction/contracts' -import Debug from 'debug' +import type { + EntryPoint, + UserOperationStruct, +} from '@account-abstraction/contracts'; +import Debug from 'debug'; +import { resolveProperties } from 'ethers/lib/utils'; -const debug = Debug('aa.postExec') +import type { NotPromise } from './ERC4337Utils'; -export async function postExecutionDump (entryPoint: EntryPoint, userOpHash: string): Promise { - const { gasPaid, gasUsed, success, userOp } = await postExecutionCheck(entryPoint, userOpHash) - /// / debug dump: - debug('==== used=', gasUsed, 'paid', gasPaid, 'over=', gasPaid - gasUsed, - 'callLen=', userOp?.callData?.length, 'initLen=', userOp?.initCode?.length, success ? 'success' : 'failed') +const debug = Debug('aa.postExec'); + +/** + * + * @param entryPoint + * @param userOpHash + */ +export async function postExecutionDump( + entryPoint: EntryPoint, + userOpHash: string, +): Promise { + const { gasPaid, gasUsed, success, userOp } = await postExecutionCheck( + entryPoint, + userOpHash, + ); + // / / debug dump: + debug( + '==== used=', + gasUsed, + 'paid', + gasPaid, + 'over=', + gasPaid - gasUsed, + 'callLen=', + userOp?.callData?.length, + 'initLen=', + userOp?.initCode?.length, + success ? 'success' : 'failed', + ); } /** @@ -20,33 +46,35 @@ export async function postExecutionDump (entryPoint: EntryPoint, userOpHash: str * @param entryPoint * @param userOpHash */ -export async function postExecutionCheck (entryPoint: EntryPoint, userOpHash: string): Promise<{ - gasUsed: number - gasPaid: number - success: boolean - userOp: NotPromise +export async function postExecutionCheck( + entryPoint: EntryPoint, + userOpHash: string, +): Promise<{ + gasUsed: number; + gasPaid: number; + success: boolean; + userOp: NotPromise; }> { - const req = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(userOpHash)) + const req = await entryPoint.queryFilter( + entryPoint.filters.UserOperationEvent(userOpHash), + ); if (req.length === 0) { - debug('postExecutionCheck: failed to read event (not mined)') + debug('postExecutionCheck: failed to read event (not mined)'); // @ts-ignore - return { gasUsed: 0, gasPaid: 0, success: false, userOp: {} } + return { gasUsed: 0, gasPaid: 0, success: false, userOp: {} }; } - const transactionReceipt = await req[0].getTransactionReceipt() + const transactionReceipt = await req[0].getTransactionReceipt(); - const tx = await req[0].getTransaction() - const { ops } = entryPoint.interface.decodeFunctionData('handleOps', tx.data) - const userOp = await resolveProperties(ops[0] as UserOperationStruct) - const { - actualGasUsed, - success - } = req[0].args - const gasPaid = actualGasUsed.toNumber() - const gasUsed = transactionReceipt.gasUsed.toNumber() + const tx = await req[0].getTransaction(); + const { ops } = entryPoint.interface.decodeFunctionData('handleOps', tx.data); + const userOp = await resolveProperties(ops[0] as UserOperationStruct); + const { actualGasUsed, success } = req[0].args; + const gasPaid = actualGasUsed.toNumber(); + const gasUsed = transactionReceipt.gasUsed.toNumber(); return { gasUsed, gasPaid, success, - userOp - } + userOp, + }; } diff --git a/src/validation-manager/BundlerCollectorTracer.ts b/src/validation-manager/BundlerCollectorTracer.ts index cc4d0fe..396b836 100644 --- a/src/validation-manager/BundlerCollectorTracer.ts +++ b/src/validation-manager/BundlerCollectorTracer.ts @@ -7,99 +7,107 @@ // where xxx is OP/STO/COD/EP/SREP/EREP/UREP/ALT, and ### is a number // the validation rules are defined in erc-aa-validation.md -import { LogCallFrame, LogContext, LogDb, LogFrameResult, LogStep, LogTracer } from './GethTracer' +import type { + LogCallFrame, + LogContext, + LogDb, + LogFrameResult, + LogStep, + LogTracer, +} from './GethTracer'; // functions available in a context of geth tracer -declare function toHex (a: any): string +declare function toHex(a: any): string; -declare function toWord (a: any): string +declare function toWord(a: any): string; -declare function toAddress (a: any): string +declare function toAddress(a: any): string; /** * return type of our BundlerCollectorTracer. * collect access and opcodes, split into "levels" based on NUMBER opcode * keccak, calls and logs are collected globally, since the levels are unimportant for them. */ -export interface BundlerTracerResult { +export type BundlerTracerResult = { /** * storage and opcode info, collected on top-level calls from EntryPoint */ - callsFromEntryPoint: TopLevelCallInfo[] + callsFromEntryPoint: TopLevelCallInfo[]; /** * values passed into KECCAK opcode */ - keccak: string[] - calls: Array - logs: LogInfo[] - debug: any[] -} + keccak: string[]; + calls: (ExitInfo | MethodInfo)[]; + logs: LogInfo[]; + debug: any[]; +}; -export interface MethodInfo { - type: string - from: string - to: string - method: string - value: any - gas: number -} +export type MethodInfo = { + type: string; + from: string; + to: string; + method: string; + value: any; + gas: number; +}; -export interface ExitInfo { - type: 'REVERT' | 'RETURN' - gasUsed: number - data: string -} +export type ExitInfo = { + type: 'REVERT' | 'RETURN'; + gasUsed: number; + data: string; +}; -export interface TopLevelCallInfo { - topLevelMethodSig: string - topLevelTargetAddress: string - opcodes: { [opcode: string]: number } - access: { [address: string]: AccessInfo } - contractSize: { [addr: string]: ContractSizeInfo } - extCodeAccessInfo: { [addr: string]: string } - oog?: boolean -} +export type TopLevelCallInfo = { + topLevelMethodSig: string; + topLevelTargetAddress: string; + opcodes: { [opcode: string]: number }; + access: { [address: string]: AccessInfo }; + contractSize: { [addr: string]: ContractSizeInfo }; + extCodeAccessInfo: { [addr: string]: string }; + oog?: boolean; +}; /** * It is illegal to access contracts with no code in validation even if it gets deployed later. * This means we need to store the {@link contractSize} of accessed addresses at the time of access. */ -export interface ContractSizeInfo { - opcode: string - contractSize: number -} +export type ContractSizeInfo = { + opcode: string; + contractSize: number; +}; -export interface AccessInfo { +export type AccessInfo = { // slot value, just prior this operation - reads: { [slot: string]: string } + reads: { [slot: string]: string }; // count of writes. - writes: { [slot: string]: number } -} + writes: { [slot: string]: number }; +}; -export interface LogInfo { - topics: string[] - data: string -} +export type LogInfo = { + topics: string[]; + data: string; +}; -interface RelevantStepData { - opcode: string - stackTop3: any[] -} +type RelevantStepData = { + opcode: string; + stackTop3: any[]; +}; /** * type-safe local storage of our collector. contains all return-value properties. * (also defines all "trace-local" variables and functions) */ -interface BundlerCollectorTracer extends LogTracer, BundlerTracerResult { - lastOp: string - lastThreeOpcodes: RelevantStepData[] - stopCollectingTopic: string - stopCollecting: boolean - currentLevel: TopLevelCallInfo - topLevelCallCounter: number - countSlot: (list: { [key: string]: number | undefined }, key: any) => void -} +type BundlerCollectorTracer = { + lastOp: string; + lastThreeOpcodes: RelevantStepData[]; + stopCollectingTopic: string; + stopCollecting: boolean; + currentLevel: TopLevelCallInfo; + topLevelCallCounter: number; + countSlot: (list: { [key: string]: number | undefined }, key: any) => void; +} & LogTracer & + BundlerTracerResult; /** * tracer to collect data for opcode banning. @@ -111,7 +119,7 @@ interface BundlerCollectorTracer extends LogTracer, BundlerTracerResult { * calls: for each call, an array of [type, from, to, value] * slots: accessed slots (on any address) */ -export function bundlerCollectorTracer (): BundlerCollectorTracer { +export function bundlerCollectorTracer(): BundlerCollectorTracer { return { callsFromEntryPoint: [], currentLevel: null as any, @@ -122,27 +130,37 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer { lastOp: '', lastThreeOpcodes: [], // event sent after all validations are done: keccak("BeforeExecution()") - stopCollectingTopic: 'bb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972', + stopCollectingTopic: + 'bb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972', stopCollecting: false, topLevelCallCounter: 0, - fault (log: LogStep, _db: LogDb): void { - this.debug.push('fault depth=', log.getDepth(), ' gas=', log.getGas(), ' cost=', log.getCost(), ' err=', log.getError()) + fault(log: LogStep, _db: LogDb): void { + this.debug.push( + 'fault depth=', + log.getDepth(), + ' gas=', + log.getGas(), + ' cost=', + log.getCost(), + ' err=', + log.getError(), + ); }, - result (_ctx: LogContext, _db: LogDb): BundlerTracerResult { + result(_ctx: LogContext, _db: LogDb): BundlerTracerResult { return { callsFromEntryPoint: this.callsFromEntryPoint, keccak: this.keccak, logs: this.logs, calls: this.calls, - debug: this.debug // for internal debugging. - } + debug: this.debug, // for internal debugging. + }; }, - enter (frame: LogCallFrame): void { + enter(frame: LogCallFrame): void { if (this.stopCollecting) { - return + return; } // this.debug.push('enter gas=', frame.getGas(), ' type=', frame.getType(), ' to=', toHex(frame.getTo()), ' in=', toHex(frame.getInput()).slice(0, 500)) this.calls.push({ @@ -151,105 +169,111 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer { to: toHex(frame.getTo()), method: toHex(frame.getInput()).slice(0, 10), gas: frame.getGas(), - value: frame.getValue() - }) + value: frame.getValue(), + }); }, - exit (frame: LogFrameResult): void { + exit(frame: LogFrameResult): void { if (this.stopCollecting) { - return + return; } this.calls.push({ type: frame.getError() != null ? 'REVERT' : 'RETURN', gasUsed: frame.getGasUsed(), - data: toHex(frame.getOutput()).slice(0, 4000) - }) + data: toHex(frame.getOutput()).slice(0, 4000), + }); }, // increment the "key" in the list. if the key is not defined yet, then set it to "1" - countSlot (list: { [key: string]: number | undefined }, key: any) { - list[key] = (list[key] ?? 0) + 1 + countSlot(list: { [key: string]: number | undefined }, key: any) { + list[key] = (list[key] ?? 0) + 1; }, - step (log: LogStep, db: LogDb): any { + step(log: LogStep, db: LogDb): any { if (this.stopCollecting) { - return + return; } - const opcode = log.op.toString() + const opcode = log.op.toString(); - const stackSize = log.stack.length() - const stackTop3 = [] + const stackSize = log.stack.length(); + const stackTop3 = []; for (let i = 0; i < 3 && i < stackSize; i++) { - stackTop3.push(log.stack.peek(i)) + stackTop3.push(log.stack.peek(i)); } - this.lastThreeOpcodes.push({ opcode, stackTop3 }) + this.lastThreeOpcodes.push({ opcode, stackTop3 }); if (this.lastThreeOpcodes.length > 3) { - this.lastThreeOpcodes.shift() + this.lastThreeOpcodes.shift(); } // this.debug.push(this.lastOp + '-' + opcode + '-' + log.getDepth() + '-' + log.getGas() + '-' + log.getCost()) - if (log.getGas() < log.getCost() || ( + if ( + log.getGas() < log.getCost() || // special rule for SSTORE with gas metering - opcode === 'SSTORE' && log.getGas() < 2300) + (opcode === 'SSTORE' && log.getGas() < 2300) ) { - this.currentLevel.oog = true + this.currentLevel.oog = true; } if (opcode === 'REVERT' || opcode === 'RETURN') { if (log.getDepth() === 1) { // exit() is not called on top-level return/revent, so we reconstruct it // from opcode - const ofs = parseInt(log.stack.peek(0).toString()) - const len = parseInt(log.stack.peek(1).toString()) - const data = toHex(log.memory.slice(ofs, ofs + len)).slice(0, 4000) + const ofs = parseInt(log.stack.peek(0).toString()); + const len = parseInt(log.stack.peek(1).toString()); + const data = toHex(log.memory.slice(ofs, ofs + len)).slice(0, 4000); // this.debug.push(opcode + ' ' + data) this.calls.push({ type: opcode, gasUsed: 0, - data - }) + data, + }); } // NOTE: flushing all history after RETURN - this.lastThreeOpcodes = [] + this.lastThreeOpcodes = []; } if (log.getDepth() === 1) { if (opcode === 'CALL' || opcode === 'STATICCALL') { // stack.peek(0) - gas - const addr = toAddress(log.stack.peek(1).toString(16)) - const topLevelTargetAddress = toHex(addr) + const addr = toAddress(log.stack.peek(1).toString(16)); + const topLevelTargetAddress = toHex(addr); // stack.peek(2) - value - const ofs = parseInt(log.stack.peek(3).toString()) + const ofs = parseInt(log.stack.peek(3).toString()); // stack.peek(4) - len - const topLevelMethodSig = toHex(log.memory.slice(ofs, ofs + 4)) + const topLevelMethodSig = toHex(log.memory.slice(ofs, ofs + 4)); - this.currentLevel = this.callsFromEntryPoint[this.topLevelCallCounter] = { + this.currentLevel = this.callsFromEntryPoint[ + this.topLevelCallCounter + ] = { topLevelMethodSig, topLevelTargetAddress, access: {}, opcodes: {}, extCodeAccessInfo: {}, - contractSize: {} - } - this.topLevelCallCounter++ + contractSize: {}, + }; + this.topLevelCallCounter++; } else if (opcode === 'LOG1') { // ignore log data ofs, len - const topic = log.stack.peek(2).toString(16) + const topic = log.stack.peek(2).toString(16); if (topic === this.stopCollectingTopic) { - this.stopCollecting = true + this.stopCollecting = true; } } - this.lastOp = '' - return + this.lastOp = ''; + return; } - const lastOpInfo = this.lastThreeOpcodes[this.lastThreeOpcodes.length - 2] + const lastOpInfo = + this.lastThreeOpcodes[this.lastThreeOpcodes.length - 2]; // store all addresses touched by EXTCODE* opcodes if (lastOpInfo?.opcode?.match(/^(EXT.*)$/) != null) { - const addr = toAddress(lastOpInfo.stackTop3[0].toString(16)) - const addrHex = toHex(addr) - const last3opcodesString = this.lastThreeOpcodes.map(x => x.opcode).join(' ') + const addr = toAddress(lastOpInfo.stackTop3[0].toString(16)); + const addrHex = toHex(addr); + const last3opcodesString = this.lastThreeOpcodes + .map((x) => x.opcode) + .join(' '); // only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case // [OP-051] if (last3opcodesString.match(/^(\w+) EXTCODESIZE ISZERO$/) == null) { - this.currentLevel.extCodeAccessInfo[addrHex] = opcode + this.currentLevel.extCodeAccessInfo[addrHex] = opcode; // this.debug.push(`potentially illegal EXTCODESIZE without ISZERO for ${addrHex}`) } else { // this.debug.push(`safe EXTCODESIZE with ISZERO for ${addrHex}`) @@ -259,87 +283,96 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer { // not using 'isPrecompiled' to only allow the ones defined by the ERC-4337 as stateless precompiles // [OP-062] const isAllowedPrecompiled: (address: any) => boolean = (address) => { - const addrHex = toHex(address) - const addressInt = parseInt(addrHex) + const addrHex = toHex(address); + const addressInt = parseInt(addrHex); // this.debug.push(`isPrecompiled address=${addrHex} addressInt=${addressInt}`) - return addressInt > 0 && addressInt < 10 - } + return addressInt > 0 && addressInt < 10; + }; // [OP-041] - if (opcode.match(/^(EXT.*|CALL|CALLCODE|DELEGATECALL|STATICCALL)$/) != null) { - const idx = opcode.startsWith('EXT') ? 0 : 1 - const addr = toAddress(log.stack.peek(idx).toString(16)) - const addrHex = toHex(addr) + if ( + opcode.match(/^(EXT.*|CALL|CALLCODE|DELEGATECALL|STATICCALL)$/) != null + ) { + const idx = opcode.startsWith('EXT') ? 0 : 1; + const addr = toAddress(log.stack.peek(idx).toString(16)); + const addrHex = toHex(addr); // this.debug.push('op=' + opcode + ' last=' + this.lastOp + ' stacksize=' + log.stack.length() + ' addr=' + addrHex) - if (this.currentLevel.contractSize[addrHex] == null && !isAllowedPrecompiled(addr)) { + if ( + this.currentLevel.contractSize[addrHex] == null && + !isAllowedPrecompiled(addr) + ) { this.currentLevel.contractSize[addrHex] = { contractSize: db.getCode(addr).length, - opcode - } + opcode, + }; } } // [OP-012] if (this.lastOp === 'GAS' && !opcode.includes('CALL')) { // count "GAS" opcode only if not followed by "CALL" - this.countSlot(this.currentLevel.opcodes, 'GAS') + this.countSlot(this.currentLevel.opcodes, 'GAS'); } if (opcode !== 'GAS') { // ignore "unimportant" opcodes: - if (opcode.match(/^(DUP\d+|PUSH\d+|SWAP\d+|POP|ADD|SUB|MUL|DIV|EQ|LTE?|S?GTE?|SLT|SH[LR]|AND|OR|NOT|ISZERO)$/) == null) { - this.countSlot(this.currentLevel.opcodes, opcode) + if ( + opcode.match( + /^(DUP\d+|PUSH\d+|SWAP\d+|POP|ADD|SUB|MUL|DIV|EQ|LTE?|S?GTE?|SLT|SH[LR]|AND|OR|NOT|ISZERO)$/, + ) == null + ) { + this.countSlot(this.currentLevel.opcodes, opcode); } } - this.lastOp = opcode + this.lastOp = opcode; if (opcode === 'SLOAD' || opcode === 'SSTORE') { - const slot = toWord(log.stack.peek(0).toString(16)) - const slotHex = toHex(slot) - const addr = log.contract.getAddress() - const addrHex = toHex(addr) - let access = this.currentLevel.access[addrHex] + const slot = toWord(log.stack.peek(0).toString(16)); + const slotHex = toHex(slot); + const addr = log.contract.getAddress(); + const addrHex = toHex(addr); + let access = this.currentLevel.access[addrHex]; if (access == null) { access = { reads: {}, - writes: {} - } - this.currentLevel.access[addrHex] = access + writes: {}, + }; + this.currentLevel.access[addrHex] = access; } if (opcode === 'SLOAD') { // read slot values before this UserOp was created // (so saving it if it was written before the first read) if (access.reads[slotHex] == null && access.writes[slotHex] == null) { - access.reads[slotHex] = toHex(db.getState(addr, slot)) + access.reads[slotHex] = toHex(db.getState(addr, slot)); } } else { - this.countSlot(access.writes, slotHex) + this.countSlot(access.writes, slotHex); } } if (opcode === 'KECCAK256') { // collect keccak on 64-byte blocks - const ofs = parseInt(log.stack.peek(0).toString()) - const len = parseInt(log.stack.peek(1).toString()) + const ofs = parseInt(log.stack.peek(0).toString()); + const len = parseInt(log.stack.peek(1).toString()); // currently, solidity uses only 2-word (6-byte) for a key. this might change.. // still, no need to return too much if (len > 20 && len < 512) { // if (len === 64) { - this.keccak.push(toHex(log.memory.slice(ofs, ofs + len))) + this.keccak.push(toHex(log.memory.slice(ofs, ofs + len))); } } else if (opcode.startsWith('LOG')) { - const count = parseInt(opcode.substring(3)) - const ofs = parseInt(log.stack.peek(0).toString()) - const len = parseInt(log.stack.peek(1).toString()) - const topics = [] + const count = parseInt(opcode.substring(3)); + const ofs = parseInt(log.stack.peek(0).toString()); + const len = parseInt(log.stack.peek(1).toString()); + const topics = []; for (let i = 0; i < count; i++) { // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - topics.push('0x' + log.stack.peek(2 + i).toString(16)) + topics.push(`0x${log.stack.peek(2 + i).toString(16)}`); } - const data = toHex(log.memory.slice(ofs, ofs + len)) + const data = toHex(log.memory.slice(ofs, ofs + len)); this.logs.push({ topics, - data - }) + data, + }); } - } - } + }, + }; } diff --git a/src/validation-manager/GethTracer.ts b/src/validation-manager/GethTracer.ts index b127fb0..1a7df4c 100644 --- a/src/validation-manager/GethTracer.ts +++ b/src/validation-manager/GethTracer.ts @@ -1,11 +1,14 @@ -import Debug from 'debug' -import { BigNumber } from 'ethers' -import { Deferrable } from '@ethersproject/properties' -import { JsonRpcProvider, TransactionRequest } from '@ethersproject/providers' -import { resolveProperties } from 'ethers/lib/utils' +import type { Deferrable } from '@ethersproject/properties'; +import type { + JsonRpcProvider, + TransactionRequest, +} from '@ethersproject/providers'; +import Debug from 'debug'; +import type { BigNumber } from 'ethers'; +import { resolveProperties } from 'ethers/lib/utils'; // from:https://geth.ethereum.org/docs/rpc/ns-debug#javascript-based-tracing -const debug = Debug('aa.tracer') +const debug = Debug('aa.tracer'); /** * a function returning a LogTracer. @@ -14,33 +17,75 @@ const debug = Debug('aa.tracer') * may only reference external functions defined by geth (see go-ethereum/eth/tracers/js): toHex, toWord, isPrecompiled, slice, toString(16) * (it is OK if original function was in typescript: we extract its value as javascript */ -type LogTracerFunc = () => LogTracer +type LogTracerFunc = () => LogTracer; // eslint-disable-next-line @typescript-eslint/naming-convention -export async function debug_traceCall (provider: JsonRpcProvider, tx: Deferrable, options: TraceOptions): Promise { - const tx1 = await resolveProperties(tx) - const traceOptions = tracer2string(options) - const ret = await provider.send('debug_traceCall', [tx1, 'latest', traceOptions]).catch(e => { - debug('ex=', e.error) - debug('tracer=', traceOptions.tracer?.toString().split('\n').map((line, index) => `${index + 1}: ${line}`).join('\n')) - throw e - }) +/** + * + * @param provider + * @param tx + * @param options + */ +export async function debug_traceCall( + provider: JsonRpcProvider, + tx: Deferrable, + options: TraceOptions, +): Promise { + const tx1 = await resolveProperties(tx); + const traceOptions = tracer2string(options); + const ret = await provider + .send('debug_traceCall', [tx1, 'latest', traceOptions]) + .catch((e) => { + debug('ex=', e.error); + debug( + 'tracer=', + traceOptions.tracer + ?.toString() + .split('\n') + .map((line, index) => `${index + 1}: ${line}`) + .join('\n'), + ); + throw e; + }); // return applyTracer(ret, options) - return ret + return ret; } // a hack for network that doesn't have traceCall: mine the transaction, and use debug_traceTransaction -export async function execAndTrace (provider: JsonRpcProvider, tx: Deferrable, options: TraceOptions): Promise { - const hash = await provider.getSigner().sendUncheckedTransaction(tx) - return await debug_traceTransaction(provider, hash, options) +/** + * + * @param provider + * @param tx + * @param options + */ +export async function execAndTrace( + provider: JsonRpcProvider, + tx: Deferrable, + options: TraceOptions, +): Promise { + const hash = await provider.getSigner().sendUncheckedTransaction(tx); + return await debug_traceTransaction(provider, hash, options); } // eslint-disable-next-line @typescript-eslint/naming-convention -export async function debug_traceTransaction (provider: JsonRpcProvider, hash: string, options: TraceOptions): Promise { - const ret = await provider.send('debug_traceTransaction', [hash, tracer2string(options)]) +/** + * + * @param provider + * @param hash + * @param options + */ +export async function debug_traceTransaction( + provider: JsonRpcProvider, + hash: string, + options: TraceOptions, +): Promise { + const ret = await provider.send('debug_traceTransaction', [ + hash, + tracer2string(options), + ]); // const tx = await provider.getTransaction(hash) // return applyTracer(tx, ret, options) - return ret + return ret; } /** @@ -48,170 +93,174 @@ export async function debug_traceTransaction (provider: JsonRpcProvider, hash: s * note that we extract the javascript body, even if the function was created as typescript * @param func */ -export function getTracerBodyString (func: LogTracerFunc): string { - const tracerFunc = func.toString() +export function getTracerBodyString(func: LogTracerFunc): string { + const tracerFunc = func.toString(); // function must return a plain object: // function xyz() { return {...}; } - const regexp = /function \w+\s*\(\s*\)\s*{\s*return\s*(\{[\s\S]+\});?\s*\}\s*$/ // (\{[\s\S]+\}); \} $/ - const match = tracerFunc.match(regexp) + const regexp = + /function \w+\s*\(\s*\)\s*{\s*return\s*(\{[\s\S]+\});?\s*\}\s*$/; // (\{[\s\S]+\}); \} $/ + const match = tracerFunc.match(regexp); if (match == null) { - throw new Error('Not a simple method returning value') + throw new Error('Not a simple method returning value'); } - let ret = match[1] + let ret = match[1]; ret = ret // .replace(/\/\/.*\n/g,'\n') // .replace(/\n\s*\n/g, '\n') - .replace(/\b(?:const|let)\b/g, '') + .replace(/\b(?:const|let)\b/g, ''); // console.log('== tracer source',ret.split('\n').map((line,index)=>`${index}: ${line}`).join('\n')) - return ret + return ret; } -function tracer2string (options: TraceOptions): TraceOptions { +/** + * + * @param options + */ +function tracer2string(options: TraceOptions): TraceOptions { if (typeof options.tracer === 'function') { return { ...options, - tracer: getTracerBodyString(options.tracer) - } - } else { - return options + tracer: getTracerBodyString(options.tracer), + }; } + return options; } // the trace options param for debug_traceCall and debug_traceTransaction -export interface TraceOptions { - disableStorage?: boolean // Setting this to true will disable storage capture (default = false). - disableStack?: boolean // Setting this to true will disable stack capture (default = false). - enableMemory?: boolean // Setting this to true will enable memory capture (default = false). - enableReturnData?: boolean // Setting this to true will enable return data capture (default = false). - tracer?: LogTracerFunc | string // Setting this will enable JavaScript-based transaction tracing, described below. If set, the previous four arguments will be ignored. - timeout?: string // Overrides the default timeout of 5 seconds for JavaScript-based tracing calls. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -} +export type TraceOptions = { + disableStorage?: boolean; // Setting this to true will disable storage capture (default = false). + disableStack?: boolean; // Setting this to true will disable stack capture (default = false). + enableMemory?: boolean; // Setting this to true will enable memory capture (default = false). + enableReturnData?: boolean; // Setting this to true will enable return data capture (default = false). + tracer?: LogTracerFunc | string; // Setting this will enable JavaScript-based transaction tracing, described below. If set, the previous four arguments will be ignored. + timeout?: string; // Overrides the default timeout of 5 seconds for JavaScript-based tracing calls. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +}; // the result type of debug_traceCall and debug_traceTransaction -export interface TraceResult { - gas: number - returnValue: string - structLogs: [TraceResultEntry] -} +export type TraceResult = { + gas: number; + returnValue: string; + structLogs: [TraceResultEntry]; +}; -export interface TraceResultEntry { - depth: number - error: string - gas: number - gasCost: number - memory?: [string] - op: string - pc: number - stack: [string] - storage?: [string] -} +export type TraceResultEntry = { + depth: number; + error: string; + gas: number; + gasCost: number; + memory?: [string]; + op: string; + pc: number; + stack: [string]; + storage?: [string]; +}; -export interface LogContext { - type: string // one of the two values CALL and CREATE - from: string // Address, sender of the transaction - to: string // Address, target of the transaction - input: Buffer // Buffer, input transaction data - gas: number // Number, gas budget of the transaction - gasUsed: number // Number, amount of gas used in executing the transaction (excludes txdata costs) - gasPrice: number // Number, gas price configured in the transaction being executed - intrinsicGas: number // Number, intrinsic gas for the transaction being executed - value: BigNumber // big.Int, amount to be transferred in wei - block: number // Number, block number - output: Buffer // Buffer, value returned from EVM - time: string // String, execution runtime +export type LogContext = { + type: string; // one of the two values CALL and CREATE + from: string; // Address, sender of the transaction + to: string; // Address, target of the transaction + input: Buffer; // Buffer, input transaction data + gas: number; // Number, gas budget of the transaction + gasUsed: number; // Number, amount of gas used in executing the transaction (excludes txdata costs) + gasPrice: number; // Number, gas price configured in the transaction being executed + intrinsicGas: number; // Number, intrinsic gas for the transaction being executed + value: BigNumber; // big.Int, amount to be transferred in wei + block: number; // Number, block number + output: Buffer; // Buffer, value returned from EVM + time: string; // String, execution runtime // And these fields are only available for tracing mined transactions (i.e. not available when doing debug_traceCall): - blockHash?: Buffer // - Buffer, hash of the block that holds the transaction being executed - txIndex?: number // - Number, index of the transaction being executed in the block - txHash?: Buffer // - Buffer, hash of the transaction being executed -} + blockHash?: Buffer; // - Buffer, hash of the block that holds the transaction being executed + txIndex?: number; // - Number, index of the transaction being executed in the block + txHash?: Buffer; // - Buffer, hash of the transaction being executed +}; -export interface LogTracer { +export type LogTracer = { // mandatory: result, fault // result is a function that takes two arguments ctx and db, and is expected to return // a JSON-serializable value to return to the RPC caller. - result: (ctx: LogContext, db: LogDb) => any + result: (ctx: LogContext, db: LogDb) => any; // fault is a function that takes two arguments, log and db, just like step and is // invoked when an error happens during the execution of an opcode which wasn’t reported in step. The method log.getError() has information about the error. - fault: (log: LogStep, db: LogDb) => void + fault: (log: LogStep, db: LogDb) => void; // optional (config is geth-level "cfg") - setup?: (config: any) => any + setup?: (config: any) => any; // optional - step?: (log: LogStep, db: LogDb) => any + step?: (log: LogStep, db: LogDb) => any; // enter and exit must be present or omitted together. - enter?: (frame: LogCallFrame) => void + enter?: (frame: LogCallFrame) => void; - exit?: (frame: LogFrameResult) => void -} + exit?: (frame: LogFrameResult) => void; +}; -export interface LogCallFrame { +export type LogCallFrame = { // - returns a string which has the type of the call frame - getType: () => string + getType: () => string; // - returns the address of the call frame sender - getFrom: () => string + getFrom: () => string; // - returns the address of the call frame target - getTo: () => string + getTo: () => string; // - returns the input as a buffer - getInput: () => string + getInput: () => string; // - returns a Number which has the amount of gas provided for the frame - getGas: () => number + getGas: () => number; // - returns a big.Int with the amount to be transferred only if available, otherwise undefined - getValue: () => BigNumber -} + getValue: () => BigNumber; +}; -export interface LogFrameResult { - getGasUsed: () => number // - returns amount of gas used throughout the frame as a Number - getOutput: () => Buffer // - returns the output as a buffer - getError: () => any // - returns an error if one occured during execution and undefined` otherwise -} +export type LogFrameResult = { + getGasUsed: () => number; // - returns amount of gas used throughout the frame as a Number + getOutput: () => Buffer; // - returns the output as a buffer + getError: () => any; // - returns an error if one occured during execution and undefined` otherwise +}; -export interface LogOpCode { - isPush: () => boolean // returns true if the opcode is a PUSHn - toString: () => string // returns the string representation of the opcode - toNumber: () => number // returns the opcode’s number -} +export type LogOpCode = { + isPush: () => boolean; // returns true if the opcode is a PUSHn + toString: () => string; // returns the string representation of the opcode + toNumber: () => number; // returns the opcode’s number +}; -export interface LogMemory { - slice: (start: number, stop: number) => any // returns the specified segment of memory as a byte slice - getUint: (offset: number) => any // returns the 32 bytes at the given offset - length: () => number // returns the memory size -} +export type LogMemory = { + slice: (start: number, stop: number) => any; // returns the specified segment of memory as a byte slice + getUint: (offset: number) => any; // returns the 32 bytes at the given offset + length: () => number; // returns the memory size +}; -export interface LogStack { - peek: (idx: number) => any // returns the idx-th element from the top of the stack (0 is the topmost element) as a big.Int - length: () => number // returns the number of elements in the stack -} +export type LogStack = { + peek: (idx: number) => any; // returns the idx-th element from the top of the stack (0 is the topmost element) as a big.Int + length: () => number; // returns the number of elements in the stack +}; -export interface LogContract { - getCaller: () => any // returns the address of the caller - getAddress: () => string // returns the address of the current contract - getValue: () => BigNumber // returns the amount of value sent from caller to contract as a big.Int - getInput: () => any // returns the input data passed to the contract -} +export type LogContract = { + getCaller: () => any; // returns the address of the caller + getAddress: () => string; // returns the address of the current contract + getValue: () => BigNumber; // returns the amount of value sent from caller to contract as a big.Int + getInput: () => any; // returns the input data passed to the contract +}; + +export type LogStep = { + op: LogOpCode; // Object, an OpCode object representing the current opcode + stack: LogStack; // Object, a structure representing the EVM execution stack + memory: LogMemory; // Object, a structure representing the contract’s memory space + contract: LogContract; // Object, an object representing the account executing the current operation -export interface LogStep { - op: LogOpCode // Object, an OpCode object representing the current opcode - stack: LogStack // Object, a structure representing the EVM execution stack - memory: LogMemory // Object, a structure representing the contract’s memory space - contract: LogContract // Object, an object representing the account executing the current operation - - getPC: () => number // returns a Number with the current program counter - getGas: () => number // returns a Number with the amount of gas remaining - getCost: () => number // returns the cost of the opcode as a Number - getDepth: () => number // returns the execution depth as a Number - getRefund: () => number // returns the amount to be refunded as a Number - getError: () => string | undefined // returns information about the error if one occured, otherwise returns undefined + getPC: () => number; // returns a Number with the current program counter + getGas: () => number; // returns a Number with the amount of gas remaining + getCost: () => number; // returns the cost of the opcode as a Number + getDepth: () => number; // returns the execution depth as a Number + getRefund: () => number; // returns the amount to be refunded as a Number + getError: () => string | undefined; // returns information about the error if one occured, otherwise returns undefined // If error is non-empty, all other fields should be ignored. -} +}; -export interface LogDb { - getBalance: (address: string) => BigNumber // - returns a big.Int with the specified account’s balance - getNonce: (address: string) => number // returns a Number with the specified account’s nonce - getCode: (address: string) => any // returns a byte slice with the code for the specified account - getState: (address: string, hash: string) => any // returns the state value for the specified account and the specified hash - exists: (address: string) => boolean // returns true if the specified address exists -} +export type LogDb = { + getBalance: (address: string) => BigNumber; // - returns a big.Int with the specified account’s balance + getNonce: (address: string) => number; // returns a Number with the specified account’s nonce + getCode: (address: string) => any; // returns a byte slice with the code for the specified account + getState: (address: string, hash: string) => any; // returns the state value for the specified account and the specified hash + exists: (address: string) => boolean; // returns true if the specified address exists +}; diff --git a/src/validation-manager/TracerResultParser.ts b/src/validation-manager/TracerResultParser.ts index 9111c98..f881198 100644 --- a/src/validation-manager/TracerResultParser.ts +++ b/src/validation-manager/TracerResultParser.ts @@ -1,55 +1,50 @@ // This file contains references to validation rules, in the format [xxx-###] // where xxx is OP/STO/COD/EP/SREP/EREP/UREP/ALT, and ### is a number // the validation rules are defined in erc-aa-validation.md -import Debug from 'debug' -import { BigNumber, BigNumberish } from 'ethers' -import { hexZeroPad, Interface, keccak256 } from 'ethers/lib/utils' -import { inspect } from 'util' - +import type { IEntryPoint } from '@account-abstraction/contracts'; import { IAccount__factory, - IEntryPoint, IEntryPoint__factory, IPaymaster__factory, - SenderCreator__factory -} from '@account-abstraction/contracts' -import { BundlerTracerResult } from './BundlerCollectorTracer' -import { - StakeInfo, - StorageMap, - UserOperation, - ValidationErrors, - mapOf, - requireCond, - toBytes32 -} from '../utils' - -import { ValidationResult } from './ValidationManager' - -const debug = Debug('aa.handler.opcodes') - -interface CallEntry { - to: string - from: string - type: string // call opcode - method: string // parsed method, or signash if unparsed - revert?: any // parsed output from REVERT - return?: any // parsed method output. - value?: BigNumberish -} - -const abi = Object.values([ - ...SenderCreator__factory.abi, - ...IEntryPoint__factory.abi, - ...IPaymaster__factory.abi -].reduce((set, entry) => { - const key = `${entry.name}(${entry.inputs.map(i => i.type).join(',')})` - // console.log('key=', key, keccak256(Buffer.from(key)).slice(0,10)) - return { - ...set, - [key]: entry - } -}, {})) as any + SenderCreator__factory, +} from '@account-abstraction/contracts'; +import Debug from 'debug'; +import type { BigNumberish } from 'ethers'; +import { BigNumber } from 'ethers'; +import { hexZeroPad, Interface, keccak256 } from 'ethers/lib/utils'; +import { inspect } from 'util'; + +import type { BundlerTracerResult } from './BundlerCollectorTracer'; +import type { ValidationResult } from './ValidationManager'; +import type { StakeInfo, StorageMap, UserOperation } from '../utils'; +import { ValidationErrors, mapOf, requireCond, toBytes32 } from '../utils'; + +const debug = Debug('aa.handler.opcodes'); + +type CallEntry = { + to: string; + from: string; + type: string; // call opcode + method: string; // parsed method, or signash if unparsed + revert?: any; // parsed output from REVERT + return?: any; // parsed method output. + value?: BigNumberish; +}; + +const abi = Object.values( + [ + ...SenderCreator__factory.abi, + ...IEntryPoint__factory.abi, + ...IPaymaster__factory.abi, + ].reduce((set, entry) => { + const key = `${entry.name}(${entry.inputs.map((i) => i.type).join(',')})`; + // console.log('key=', key, keccak256(Buffer.from(key)).slice(0,10)) + return { + ...set, + [key]: entry, + }; + }, {}), +) as any; /** * parse all call operation in the trace. @@ -59,70 +54,82 @@ const abi = Object.values([ * @param tracerResults * @param abi */ -function parseCallStack ( - tracerResults: BundlerTracerResult -): CallEntry[] { - const xfaces = new Interface(abi) - - function callCatch (x: () => T, def: T1): T | T1 { +function parseCallStack(tracerResults: BundlerTracerResult): CallEntry[] { + const xfaces = new Interface(abi); + + /** + * + * @param x + * @param def + */ + function callCatch(x: () => T, def: T1): T | T1 { try { - return x() + return x(); } catch { - return def + return def; } } - const out: CallEntry[] = [] - const stack: any[] = [] + const out: CallEntry[] = []; + const stack: any[] = []; tracerResults.calls - .filter(x => !x.type.startsWith('depth')) - .forEach(c => { + .filter((x) => !x.type.startsWith('depth')) + .forEach((c) => { if (c.type.match(/REVERT|RETURN/) != null) { const top = stack.splice(-1)[0] ?? { type: 'top', - method: 'validateUserOp' - } - const returnData: string = (c as any).data + method: 'validateUserOp', + }; + const returnData: string = (c as any).data; if (top.type.match(/CREATE/) != null) { out.push({ to: top.to, from: top.from, type: top.type, method: '', - return: `len=${returnData.length}` - }) + return: `len=${returnData.length}`, + }); } else { - const method = callCatch(() => xfaces.getFunction(top.method), top.method) + const method = callCatch( + () => xfaces.getFunction(top.method), + top.method, + ); if (c.type === 'REVERT') { - const parsedError = callCatch(() => xfaces.parseError(returnData), returnData) + const parsedError = callCatch( + () => xfaces.parseError(returnData), + returnData, + ); out.push({ to: top.to, from: top.from, type: top.type, method: method.name, value: top.value, - revert: parsedError - }) + revert: parsedError, + }); } else { - const ret = callCatch(() => xfaces.decodeFunctionResult(method, returnData), returnData) + const ret = callCatch( + () => xfaces.decodeFunctionResult(method, returnData), + returnData, + ); out.push({ to: top.to, from: top.from, type: top.type, value: top.value, method: method.name ?? method, - return: ret - }) + return: ret, + }); } } } else { - stack.push(c) + stack.push(c); } - }) + }); // TODO: verify that stack is empty at the end. - return out + return out; } /** @@ -130,294 +137,418 @@ function parseCallStack ( * keccak( A || ...) is associated with "A" * removed rule: keccak( ... || ASSOC ) (for a previously associated hash) is also associated with "A" * - * @param stakeInfoEntities stake info for (factory, account, paymaster). factory and paymaster can be null. - * @param keccak array of buffers that were given to keccak in the transaction + * @param stakeInfoEntities - stake info for (factory, account, paymaster). factory and paymaster can be null. + * @param keccak - array of buffers that were given to keccak in the transaction */ -function parseEntitySlots (stakeInfoEntities: { [addr: string]: StakeInfo | undefined }, keccak: string[]): { - [addr: string]: Set +function parseEntitySlots( + stakeInfoEntities: { [addr: string]: StakeInfo | undefined }, + keccak: string[], +): { + [addr: string]: Set; } { // for each entity (sender, factory, paymaster), hold the valid slot addresses // valid: the slot was generated by keccak(entity || ...) - const entitySlots: { [addr: string]: Set } = {} + const entitySlots: { [addr: string]: Set } = {}; - keccak.forEach(k => { - Object.values(stakeInfoEntities).forEach(info => { - const addr = info?.addr?.toLowerCase() - if (addr == null) return - const addrPadded = toBytes32(addr) + keccak.forEach((k) => { + Object.values(stakeInfoEntities).forEach((info) => { + const addr = info?.addr?.toLowerCase(); + if (addr == null) { + return; + } + const addrPadded = toBytes32(addr); if (entitySlots[addr] == null) { - entitySlots[addr] = new Set() + entitySlots[addr] = new Set(); } - const currentEntitySlots = entitySlots[addr] + const currentEntitySlots = entitySlots[addr]; // valid slot: the slot was generated by keccak(entityAddr || ...) if (k.startsWith(addrPadded)) { // console.log('added mapping (balance) slot', value) - currentEntitySlots.add(keccak256(k)) + currentEntitySlots.add(keccak256(k)); } // disabled 2nd rule: .. or by keccak( ... || OWN) where OWN is previous allowed slot // if (k.length === 130 && currentEntitySlots.has(k.slice(-64))) { // // console.log('added double-mapping (allowance) slot', value) // currentEntitySlots.add(value) // } - }) - }) + }); + }); - return entitySlots + return entitySlots; } // method-signature for calls from entryPoint const callsFromEntryPointMethodSigs: { [key: string]: string } = { factory: SenderCreator__factory.createInterface().getSighash('createSender'), account: IAccount__factory.createInterface().getSighash('validateUserOp'), - paymaster: IPaymaster__factory.createInterface().getSighash('validatePaymasterUserOp') -} + paymaster: IPaymaster__factory.createInterface().getSighash( + 'validatePaymasterUserOp', + ), +}; /** * parse collected simulation traces and revert if they break our rules - * @param userOp the userOperation that was used in this simulation - * @param tracerResults the tracer return value - * @param validationResult output from simulateValidation - * @param entryPoint the entryPoint that hosted the "simulatedValidation" traced call. - * @return list of contract addresses referenced by this UserOp + * @param userOp - the userOperation that was used in this simulation + * @param tracerResults - the tracer return value + * @param validationResult - output from simulateValidation + * @param entryPoint - the entryPoint that hosted the "simulatedValidation" traced call. + * @returns list of contract addresses referenced by this UserOp */ -export function tracerResultParser ( +export function tracerResultParser( userOp: UserOperation, tracerResults: BundlerTracerResult, validationResult: ValidationResult, - entryPoint: IEntryPoint + entryPoint: IEntryPoint, ): [string[], StorageMap] { - debug('=== simulation result:', inspect(tracerResults, true, 10, true)) + debug('=== simulation result:', inspect(tracerResults, true, 10, true)); // todo: block access to no-code addresses (might need update to tracer) - const entryPointAddress = entryPoint.address.toLowerCase() + const entryPointAddress = entryPoint.address.toLowerCase(); // opcodes from [OP-011] - const bannedOpCodes = new Set(['GASPRICE', 'GASLIMIT', 'DIFFICULTY', 'TIMESTAMP', 'BASEFEE', 'BLOCKHASH', 'NUMBER', 'SELFBALANCE', 'BALANCE', 'ORIGIN', 'GAS', 'CREATE', 'COINBASE', 'SELFDESTRUCT', 'RANDOM', 'PREVRANDAO', 'INVALID']) + const bannedOpCodes = new Set([ + 'GASPRICE', + 'GASLIMIT', + 'DIFFICULTY', + 'TIMESTAMP', + 'BASEFEE', + 'BLOCKHASH', + 'NUMBER', + 'SELFBALANCE', + 'BALANCE', + 'ORIGIN', + 'GAS', + 'CREATE', + 'COINBASE', + 'SELFDESTRUCT', + 'RANDOM', + 'PREVRANDAO', + 'INVALID', + ]); // eslint-disable-next-line @typescript-eslint/no-base-to-string if (Object.values(tracerResults.callsFromEntryPoint).length < 1) { - throw new Error('Unexpected traceCall result: no calls from entrypoint.') + throw new Error('Unexpected traceCall result: no calls from entrypoint.'); } - const callStack = parseCallStack(tracerResults) + const callStack = parseCallStack(tracerResults); // [OP-052], [OP-053] - const callInfoEntryPoint = callStack.find(call => - call.to === entryPointAddress && call.from !== entryPointAddress && - (call.method !== '0x' && call.method !== 'depositTo')) + const callInfoEntryPoint = callStack.find( + (call) => + call.to === entryPointAddress && + call.from !== entryPointAddress && + call.method !== '0x' && + call.method !== 'depositTo', + ); // [OP-054] - requireCond(callInfoEntryPoint == null, + requireCond( + callInfoEntryPoint == null, `illegal call into EntryPoint during validation ${callInfoEntryPoint?.method}`, - ValidationErrors.OpcodeValidation - ) + ValidationErrors.OpcodeValidation, + ); // [OP-061] const illegalNonZeroValueCall = callStack.find( - call => - call.to !== entryPointAddress && - !BigNumber.from(call.value ?? 0).eq(0)) + (call) => + call.to !== entryPointAddress && !BigNumber.from(call.value ?? 0).eq(0), + ); requireCond( illegalNonZeroValueCall == null, 'May not may CALL with value', - ValidationErrors.OpcodeValidation) + ValidationErrors.OpcodeValidation, + ); - const sender = userOp.sender.toLowerCase() + const sender = userOp.sender.toLowerCase(); // stake info per "number" level (factory, sender, paymaster) // we only use stake info if we notice a memory reference that require stake const stakeInfoEntities = { factory: validationResult.factoryInfo, account: validationResult.senderInfo, - paymaster: validationResult.paymasterInfo - } + paymaster: validationResult.paymasterInfo, + }; - const entitySlots: { [addr: string]: Set } = parseEntitySlots(stakeInfoEntities, tracerResults.keccak) + const entitySlots: { [addr: string]: Set } = parseEntitySlots( + stakeInfoEntities, + tracerResults.keccak, + ); Object.entries(stakeInfoEntities).forEach(([entityTitle, entStakes]) => { - const entityAddr = (entStakes?.addr ?? '').toLowerCase() - const currentNumLevel = tracerResults.callsFromEntryPoint.find(info => info.topLevelMethodSig === callsFromEntryPointMethodSigs[entityTitle]) + const entityAddr = (entStakes?.addr ?? '').toLowerCase(); + const currentNumLevel = tracerResults.callsFromEntryPoint.find( + (info) => + info.topLevelMethodSig === callsFromEntryPointMethodSigs[entityTitle], + ); if (currentNumLevel == null) { if (entityTitle === 'account') { // should never happen... only factory, paymaster are optional. - throw new Error('missing trace into validateUserOp') + throw new Error('missing trace into validateUserOp'); } - return + return; } - const opcodes = currentNumLevel.opcodes - const access = currentNumLevel.access + const { opcodes } = currentNumLevel; + const { access } = currentNumLevel; // [OP-020] - requireCond(!(currentNumLevel.oog ?? false), - `${entityTitle} internally reverts on oog`, ValidationErrors.OpcodeValidation) + requireCond( + !(currentNumLevel.oog ?? false), + `${entityTitle} internally reverts on oog`, + ValidationErrors.OpcodeValidation, + ); // opcodes from [OP-011] - Object.keys(opcodes).forEach(opcode => - requireCond(!bannedOpCodes.has(opcode), `${entityTitle} uses banned opcode: ${opcode}`, ValidationErrors.OpcodeValidation) - ) + Object.keys(opcodes).forEach((opcode) => + requireCond( + !bannedOpCodes.has(opcode), + `${entityTitle} uses banned opcode: ${opcode}`, + ValidationErrors.OpcodeValidation, + ), + ); // [OP-031] if (entityTitle === 'factory') { - requireCond((opcodes.CREATE2 ?? 0) <= 1, `${entityTitle} with too many CREATE2`, ValidationErrors.OpcodeValidation) + requireCond( + (opcodes.CREATE2 ?? 0) <= 1, + `${entityTitle} with too many CREATE2`, + ValidationErrors.OpcodeValidation, + ); } else { - requireCond(opcodes.CREATE2 == null, `${entityTitle} uses banned opcode: CREATE2`, ValidationErrors.OpcodeValidation) + requireCond( + opcodes.CREATE2 == null, + `${entityTitle} uses banned opcode: CREATE2`, + ValidationErrors.OpcodeValidation, + ); } - Object.entries(access).forEach(([addr, { - reads, - writes - }]) => { + Object.entries(access).forEach(([addr, { reads, writes }]) => { // testing read/write access on contract "addr" if (addr === sender) { // allowed to access sender's storage // [STO-010] - return + return; } if (addr === entryPointAddress) { // ignore storage access on entryPoint (balance/deposit of entities. // we block them on method calls: only allowed to deposit, never to read - return + return; } // return true if the given slot is associated with the given address, given the known keccak operations: // @param slot the SLOAD/SSTORE slot address we're testing // @param addr - the address we try to check for association with // @param reverseKeccak - a mapping we built for keccak values that contained the address - function associatedWith (slot: string, addr: string, entitySlots: { [addr: string]: Set }): boolean { - const addrPadded = hexZeroPad(addr, 32).toLowerCase() + /** + * + * @param slot + * @param addr + */ + function associatedWith( + slot: string, + addr: string, + entitySlots: { [addr: string]: Set }, + ): boolean { + const addrPadded = hexZeroPad(addr, 32).toLowerCase(); if (slot === addrPadded) { - return true + return true; } - const k = entitySlots[addr] + const k = entitySlots[addr]; if (k == null) { - return false + return false; } - const slotN = BigNumber.from(slot) + const slotN = BigNumber.from(slot); // scan all slot entries to check of the given slot is within a structure, starting at that offset. // assume a maximum size on a (static) structure size. for (const k1 of k.keys()) { - const kn = BigNumber.from(k1) + const kn = BigNumber.from(k1); if (slotN.gte(kn) && slotN.lt(kn.add(128))) { - return true + return true; } } - return false + return false; } debug('dump keccak calculations and reads', { entityTitle, entityAddr, - k: mapOf(tracerResults.keccak, k => keccak256(k)), - reads - }) + k: mapOf(tracerResults.keccak, (k) => keccak256(k)), + reads, + }); // scan all slots. find a referenced slot // at the end of the scan, we will check if the entity has stake, and report that slot if not. - let requireStakeSlot: string | undefined - [...Object.keys(writes), ...Object.keys(reads)].forEach(slot => { + let requireStakeSlot: string | undefined; + [...Object.keys(writes), ...Object.keys(reads)].forEach((slot) => { // slot associated with sender is allowed (e.g. token.balanceOf(sender) // but during initial UserOp (where there is an initCode), it is allowed only for staked entity if (associatedWith(slot, sender, entitySlots)) { if (userOp.initCode.length > 2) { // special case: account.validateUserOp is allowed to use assoc storage if factory is staked. // [STO-022], [STO-021] - if (!(entityAddr === sender && isStaked(stakeInfoEntities.factory))) { - requireStakeSlot = slot + if ( + !(entityAddr === sender && isStaked(stakeInfoEntities.factory)) + ) { + requireStakeSlot = slot; } } } else if (associatedWith(slot, entityAddr, entitySlots)) { // [STO-032] // accessing a slot associated with entityAddr (e.g. token.balanceOf(paymaster) - requireStakeSlot = slot + requireStakeSlot = slot; } else if (addr === entityAddr) { // [STO-031] // accessing storage member of entity itself requires stake. - requireStakeSlot = slot + requireStakeSlot = slot; } else if (writes[slot] == null) { // [STO-033]: staked entity have read-only access to any storage in non-entity contract. - requireStakeSlot = slot + requireStakeSlot = slot; } else { // accessing arbitrary storage of another contract is not allowed - const readWrite = Object.keys(writes).includes(addr) ? 'write to' : 'read from' - requireCond(false, - `${entityTitle} has forbidden ${readWrite} ${nameAddr(addr, entityTitle)} slot ${slot}`, - ValidationErrors.OpcodeValidation, { [entityTitle]: entStakes?.addr }) + const readWrite = Object.keys(writes).includes(addr) + ? 'write to' + : 'read from'; + requireCond( + false, + `${entityTitle} has forbidden ${readWrite} ${nameAddr( + addr, + entityTitle, + )} slot ${slot}`, + ValidationErrors.OpcodeValidation, + { [entityTitle]: entStakes?.addr }, + ); } - }) + }); // if addr is current account/paymaster/factory, then return that title // otherwise, return addr as-is - function nameAddr (addr: string, currentEntity: string): string { - const [title] = Object.entries(stakeInfoEntities).find(([title, info]) => - info?.addr.toLowerCase() === addr.toLowerCase()) ?? [] - - return title ?? addr + /** + * + * @param addr + * @param currentEntity + */ + function nameAddr(addr: string, currentEntity: string): string { + const [title] = + Object.entries(stakeInfoEntities).find( + ([title, info]) => info?.addr.toLowerCase() === addr.toLowerCase(), + ) ?? []; + + return title ?? addr; } - requireCondAndStake(requireStakeSlot != null, entStakes, - `unstaked ${entityTitle} accessed ${nameAddr(addr, entityTitle)} slot ${requireStakeSlot}`) - }) + requireCondAndStake( + requireStakeSlot != null, + entStakes, + `unstaked ${entityTitle} accessed ${nameAddr( + addr, + entityTitle, + )} slot ${requireStakeSlot}`, + ); + }); // [EREP-050] if (entityTitle === 'paymaster') { - const validatePaymasterUserOp = callStack.find(call => call.method === 'validatePaymasterUserOp' && call.to === entityAddr) - const context = validatePaymasterUserOp?.return?.context - requireCondAndStake(context != null && context !== '0x', entStakes, - 'unstaked paymaster must not return context') + const validatePaymasterUserOp = callStack.find( + (call) => + call.method === 'validatePaymasterUserOp' && call.to === entityAddr, + ); + const context = validatePaymasterUserOp?.return?.context; + requireCondAndStake( + context != null && context !== '0x', + entStakes, + 'unstaked paymaster must not return context', + ); } // check if the given entity is staked - function isStaked (entStake?: StakeInfo): boolean { - return entStake != null && BigNumber.from(1).lte(entStake.stake) && BigNumber.from(1).lte(entStake.unstakeDelaySec) + /** + * + * @param entStake + */ + function isStaked(entStake?: StakeInfo): boolean { + return ( + entStake != null && + BigNumber.from(1).lte(entStake.stake) && + BigNumber.from(1).lte(entStake.unstakeDelaySec) + ); } // helper method: if condition is true, then entity must be staked. - function requireCondAndStake (cond: boolean, entStake: StakeInfo | undefined, failureMessage: string): void { + /** + * + * @param cond + * @param entStake + * @param failureMessage + */ + function requireCondAndStake( + cond: boolean, + entStake: StakeInfo | undefined, + failureMessage: string, + ): void { if (!cond) { - return + return; } if (entStake == null) { - throw new Error(`internal: ${entityTitle} not in userOp, but has storage accesses in ${JSON.stringify(access)}`) + throw new Error( + `internal: ${entityTitle} not in userOp, but has storage accesses in ${JSON.stringify( + access, + )}`, + ); } - requireCond(isStaked(entStake), - failureMessage, ValidationErrors.OpcodeValidation, { [entityTitle]: entStakes?.addr }) + requireCond( + isStaked(entStake), + failureMessage, + ValidationErrors.OpcodeValidation, + { [entityTitle]: entStakes?.addr }, + ); // TODO: check real minimum stake values } // the only contract we allow to access before its deployment is the "sender" itself, which gets created. - let illegalZeroCodeAccess: any + let illegalZeroCodeAccess: any; for (const addr of Object.keys(currentNumLevel.contractSize)) { // [OP-042] - if (addr !== sender && currentNumLevel.contractSize[addr].contractSize <= 2) { - illegalZeroCodeAccess = currentNumLevel.contractSize[addr] - illegalZeroCodeAccess.address = addr - break + if ( + addr !== sender && + currentNumLevel.contractSize[addr].contractSize <= 2 + ) { + illegalZeroCodeAccess = currentNumLevel.contractSize[addr]; + illegalZeroCodeAccess.address = addr; + break; } } // [OP-041] requireCond( illegalZeroCodeAccess == null, - `${entityTitle} accesses un-deployed contract address ${illegalZeroCodeAccess?.address as string} with opcode ${illegalZeroCodeAccess?.opcode as string}`, ValidationErrors.OpcodeValidation) + `${entityTitle} accesses un-deployed contract address ${ + illegalZeroCodeAccess?.address as string + } with opcode ${illegalZeroCodeAccess?.opcode as string}`, + ValidationErrors.OpcodeValidation, + ); - let illegalEntryPointCodeAccess + let illegalEntryPointCodeAccess; for (const addr of Object.keys(currentNumLevel.extCodeAccessInfo)) { if (addr === entryPointAddress) { - illegalEntryPointCodeAccess = currentNumLevel.extCodeAccessInfo[addr] - break + illegalEntryPointCodeAccess = currentNumLevel.extCodeAccessInfo[addr]; + break; } } requireCond( illegalEntryPointCodeAccess == null, - `${entityTitle} accesses EntryPoint contract address ${entryPointAddress} with opcode ${illegalEntryPointCodeAccess}`, ValidationErrors.OpcodeValidation) - }) + `${entityTitle} accesses EntryPoint contract address ${entryPointAddress} with opcode ${illegalEntryPointCodeAccess}`, + ValidationErrors.OpcodeValidation, + ); + }); // return list of contract addresses by this UserOp. already known not to contain zero-sized addresses. - const addresses = tracerResults.callsFromEntryPoint.flatMap(level => Object.keys(level.contractSize)) - const storageMap: StorageMap = {} - tracerResults.callsFromEntryPoint.forEach(level => { - Object.keys(level.access).forEach(addr => { - storageMap[addr] = storageMap[addr] ?? level.access[addr].reads - }) - }) - return [addresses, storageMap] + const addresses = tracerResults.callsFromEntryPoint.flatMap((level) => + Object.keys(level.contractSize), + ); + const storageMap: StorageMap = {}; + tracerResults.callsFromEntryPoint.forEach((level) => { + Object.keys(level.access).forEach((addr) => { + storageMap[addr] = storageMap[addr] ?? level.access[addr].reads; + }); + }); + return [addresses, storageMap]; } diff --git a/src/validation-manager/ValidationManager.ts b/src/validation-manager/ValidationManager.ts index e139115..18ad31c 100644 --- a/src/validation-manager/ValidationManager.ts +++ b/src/validation-manager/ValidationManager.ts @@ -1,86 +1,99 @@ -import { BigNumber, BigNumberish, BytesLike, ethers } from 'ethers' -import { JsonRpcProvider } from '@ethersproject/providers' -import Debug from 'debug' - -import { IEntryPoint } from '@account-abstraction/contracts' +import type { IEntryPoint } from '@account-abstraction/contracts'; +import type { JsonRpcProvider } from '@ethersproject/providers'; +import Debug from 'debug'; +import { BigNumber, ethers } from 'ethers'; +import type { BigNumberish, BytesLike } from 'ethers'; + +import type { BundlerTracerResult, ExitInfo } from './BundlerCollectorTracer'; +import { bundlerCollectorTracer } from './BundlerCollectorTracer'; +import { debug_traceCall } from './GethTracer'; +import { tracerResultParser } from './TracerResultParser'; +import { CodeHashGetter__factory } from '../contract-types'; +import { calcPreVerificationGas } from '../sdk'; import { AddressZero, - ReferencedCodeHashes, RpcError, - StakeInfo, - StorageMap, - UserOperation, ValidationErrors, decodeErrorReason, getAddr, requireCond, - runContractScript -} from '../utils' -import { CodeHashGetter__factory } from '../contract-types' -import { calcPreVerificationGas } from '../sdk' - -import { tracerResultParser } from './TracerResultParser' -import { BundlerTracerResult, bundlerCollectorTracer, ExitInfo } from './BundlerCollectorTracer' -import { debug_traceCall } from './GethTracer' + runContractScript, +} from '../utils'; +import type { + ReferencedCodeHashes, + StakeInfo, + StorageMap, + UserOperation, +} from '../utils'; -const debug = Debug('aa.mgr.validate') +const debug = Debug('aa.mgr.validate'); // how much time into the future a UserOperation must be valid in order to be accepted -const VALID_UNTIL_FUTURE_SECONDS = 30 +const VALID_UNTIL_FUTURE_SECONDS = 30; /** * result from successful simulateValidation */ -export interface ValidationResult { +export type ValidationResult = { returnInfo: { - preOpGas: BigNumberish - prefund: BigNumberish - sigFailed: boolean - validAfter: number - validUntil: number - } - - senderInfo: StakeInfo - factoryInfo?: StakeInfo - paymasterInfo?: StakeInfo - aggregatorInfo?: StakeInfo -} - -export interface ValidateUserOpResult extends ValidationResult { - - referencedContracts: ReferencedCodeHashes - storageMap: StorageMap -} - -const HEX_REGEX = /^0x[a-fA-F\d]*$/i + preOpGas: BigNumberish; + prefund: BigNumberish; + sigFailed: boolean; + validAfter: number; + validUntil: number; + }; + + senderInfo: StakeInfo; + factoryInfo?: StakeInfo; + paymasterInfo?: StakeInfo; + aggregatorInfo?: StakeInfo; +}; + +export type ValidateUserOpResult = { + referencedContracts: ReferencedCodeHashes; + storageMap: StorageMap; +} & ValidationResult; + +const HEX_REGEX = /^0x[a-fA-F\d]*$/i; export class ValidationManager { - constructor ( - readonly entryPoint: IEntryPoint, - readonly unsafe: boolean - ) {} + constructor(readonly entryPoint: IEntryPoint, readonly unsafe: boolean) {} // standard eth_call to simulateValidation - async _callSimulateValidation (userOp: UserOperation): Promise { - const errorResult = await this.entryPoint.callStatic.simulateValidation(userOp, { gasLimit: 10e6 }).catch(e => e) - return this._parseErrorResult(userOp, errorResult) + async _callSimulateValidation( + userOp: UserOperation, + ): Promise { + const errorResult = await this.entryPoint.callStatic + .simulateValidation(userOp, { gasLimit: 10e6 }) + .catch((e) => e); + return this._parseErrorResult(userOp, errorResult); } - _parseErrorResult (userOp: UserOperation, errorResult: { errorName: string, errorArgs: any }): ValidationResult { + _parseErrorResult( + userOp: UserOperation, + errorResult: { errorName: string; errorArgs: any }, + ): ValidationResult { if (!errorResult?.errorName?.startsWith('ValidationResult')) { // parse it as FailedOp // if its FailedOp, then we have the paymaster param... otherwise its an Error(string) - let paymaster = errorResult.errorArgs.paymaster + let { paymaster } = errorResult.errorArgs; if (paymaster === AddressZero) { - paymaster = undefined + paymaster = undefined; } // eslint-disable-next-line const msg: string = errorResult.errorArgs?.reason ?? errorResult.toString() if (paymaster == null) { - throw new RpcError(`account validation failed: ${msg}`, ValidationErrors.SimulateValidation) + throw new RpcError( + `account validation failed: ${msg}`, + ValidationErrors.SimulateValidation, + ); } else { - throw new RpcError(`paymaster validation failed: ${msg}`, ValidationErrors.SimulatePaymasterValidation, { paymaster }) + throw new RpcError( + `paymaster validation failed: ${msg}`, + ValidationErrors.SimulatePaymasterValidation, + { paymaster }, + ); } } @@ -89,86 +102,114 @@ export class ValidationManager { senderInfo, factoryInfo, paymasterInfo, - aggregatorInfo // may be missing (exists only SimulationResultWithAggregator - } = errorResult.errorArgs + aggregatorInfo, // may be missing (exists only SimulationResultWithAggregator + } = errorResult.errorArgs; // extract address from "data" (first 20 bytes) // add it as "addr" member to the "stakeinfo" struct // if no address, then return "undefined" instead of struct. - function fillEntity (data: BytesLike, info: StakeInfo): StakeInfo | undefined { - const addr = getAddr(data) + /** + * + * @param data + * @param info + */ + function fillEntity( + data: BytesLike, + info: StakeInfo, + ): StakeInfo | undefined { + const addr = getAddr(data); return addr == null ? undefined : { ...info, - addr - } + addr, + }; } return { returnInfo, senderInfo: { ...senderInfo, - addr: userOp.sender + addr: userOp.sender, }, factoryInfo: fillEntity(userOp.initCode, factoryInfo), paymasterInfo: fillEntity(userOp.paymasterAndData, paymasterInfo), - aggregatorInfo: fillEntity(aggregatorInfo?.actualAggregator, aggregatorInfo?.stakeInfo) - } + aggregatorInfo: fillEntity( + aggregatorInfo?.actualAggregator, + aggregatorInfo?.stakeInfo, + ), + }; } - async _geth_traceCall_SimulateValidation (userOp: UserOperation): Promise<[ValidationResult, BundlerTracerResult]> { - const provider = this.entryPoint.provider as JsonRpcProvider - const simulateCall = this.entryPoint.interface.encodeFunctionData('simulateValidation', [userOp]) - - const simulationGas = BigNumber.from(userOp.preVerificationGas).add(userOp.verificationGasLimit) - - const tracerResult: BundlerTracerResult = await debug_traceCall(provider, { - from: ethers.constants.AddressZero, - to: this.entryPoint.address, - data: simulateCall, - gasLimit: simulationGas - }, { tracer: bundlerCollectorTracer }) + async _geth_traceCall_SimulateValidation( + userOp: UserOperation, + ): Promise<[ValidationResult, BundlerTracerResult]> { + const provider = this.entryPoint.provider as JsonRpcProvider; + const simulateCall = this.entryPoint.interface.encodeFunctionData( + 'simulateValidation', + [userOp], + ); + + const simulationGas = BigNumber.from(userOp.preVerificationGas).add( + userOp.verificationGasLimit, + ); + + const tracerResult: BundlerTracerResult = await debug_traceCall( + provider, + { + from: ethers.constants.AddressZero, + to: this.entryPoint.address, + data: simulateCall, + gasLimit: simulationGas, + }, + { tracer: bundlerCollectorTracer }, + ); - const lastResult = tracerResult.calls.slice(-1)[0] + const lastResult = tracerResult.calls.slice(-1)[0]; if (lastResult.type !== 'REVERT') { - throw new Error('Invalid response. simulateCall must revert') + throw new Error('Invalid response. simulateCall must revert'); } - const data = (lastResult as ExitInfo).data + const { data } = lastResult as ExitInfo; // Hack to handle SELFDESTRUCT until we fix entrypoint if (data === '0x') { - return [data as any, tracerResult] + return [data as any, tracerResult]; } try { - const { - name: errorName, - args: errorArgs - } = this.entryPoint.interface.parseError(data) - const errFullName = `${errorName}(${errorArgs.toString()})` + const { name: errorName, args: errorArgs } = + this.entryPoint.interface.parseError(data); + const errFullName = `${errorName}(${errorArgs.toString()})`; const errorResult = this._parseErrorResult(userOp, { errorName, - errorArgs - }) + errorArgs, + }); if (!errorName.includes('Result')) { // a real error, not a result. - throw new Error(errFullName) + throw new Error(errFullName); } - debug('==dump tree=', JSON.stringify(tracerResult, null, 2) - .replace(new RegExp(userOp.sender.toLowerCase()), '{sender}') - .replace(new RegExp(getAddr(userOp.paymasterAndData) ?? '--no-paymaster--'), '{paymaster}') - .replace(new RegExp(getAddr(userOp.initCode) ?? '--no-initcode--'), '{factory}') - ) + debug( + '==dump tree=', + JSON.stringify(tracerResult, null, 2) + .replace(new RegExp(userOp.sender.toLowerCase()), '{sender}') + .replace( + new RegExp(getAddr(userOp.paymasterAndData) ?? '--no-paymaster--'), + '{paymaster}', + ) + .replace( + new RegExp(getAddr(userOp.initCode) ?? '--no-initcode--'), + '{factory}', + ), + ); // console.log('==debug=', ...tracerResult.numberLevels.forEach(x=>x.access), 'sender=', userOp.sender, 'paymaster=', hexlify(userOp.paymasterAndData)?.slice(0, 42)) // errorResult is "ValidationResult" - return [errorResult, tracerResult] + return [errorResult, tracerResult]; } catch (e: any) { // if already parsed, throw as is if (e.code != null) { - throw e + throw e; } // not a known error of EntryPoint (probably, only Error(string), since FailedOp is handled above) - const err = decodeErrorReason(data) - throw new RpcError(err != null ? err.message : data, 111) + const err = decodeErrorReason(data); + throw new RpcError(err != null ? err.message : data, 111); } } @@ -177,78 +218,106 @@ export class ValidationManager { * should also handle unmodified memory (e.g. by referencing cached storage in the mempool * one item to check that was un-modified is the aggregator.. * @param userOp + * @param previousCodeHashes + * @param checkStakes */ - async validateUserOp (userOp: UserOperation, previousCodeHashes?: ReferencedCodeHashes, checkStakes = true): Promise { + async validateUserOp( + userOp: UserOperation, + previousCodeHashes?: ReferencedCodeHashes, + checkStakes = true, + ): Promise { if (previousCodeHashes != null && previousCodeHashes.addresses.length > 0) { - const { hash: codeHashes } = await this.getCodeHashes(previousCodeHashes.addresses) + const { hash: codeHashes } = await this.getCodeHashes( + previousCodeHashes.addresses, + ); // [COD-010] - requireCond(codeHashes === previousCodeHashes.hash, + requireCond( + codeHashes === previousCodeHashes.hash, 'modified code after first validation', - ValidationErrors.OpcodeValidation) + ValidationErrors.OpcodeValidation, + ); } - let res: ValidationResult + let res: ValidationResult; let codeHashes: ReferencedCodeHashes = { addresses: [], - hash: '' - } - let storageMap: StorageMap = {} + hash: '', + }; + let storageMap: StorageMap = {}; if (!this.unsafe) { - let tracerResult: BundlerTracerResult - [res, tracerResult] = await this._geth_traceCall_SimulateValidation(userOp) - let contractAddresses: string[] - [contractAddresses, storageMap] = tracerResultParser(userOp, tracerResult, res, this.entryPoint) + let tracerResult: BundlerTracerResult; + [res, tracerResult] = await this._geth_traceCall_SimulateValidation( + userOp, + ); + let contractAddresses: string[]; + [contractAddresses, storageMap] = tracerResultParser( + userOp, + tracerResult, + res, + this.entryPoint, + ); // if no previous contract hashes, then calculate hashes of contracts if (previousCodeHashes == null) { - codeHashes = await this.getCodeHashes(contractAddresses) + codeHashes = await this.getCodeHashes(contractAddresses); } - if (res as any === '0x') { - throw new Error('simulateValidation reverted with no revert string!') + if ((res as any) === '0x') { + throw new Error('simulateValidation reverted with no revert string!'); } } else { // NOTE: this mode doesn't do any opcode checking and no stake checking! - res = await this._callSimulateValidation(userOp) + res = await this._callSimulateValidation(userOp); } - requireCond(!res.returnInfo.sigFailed, + requireCond( + !res.returnInfo.sigFailed, 'Invalid UserOp signature or paymaster signature', - ValidationErrors.InvalidSignature) + ValidationErrors.InvalidSignature, + ); - const now = Math.floor(Date.now() / 1000) - requireCond(res.returnInfo.validAfter <= now, + const now = Math.floor(Date.now() / 1000); + requireCond( + res.returnInfo.validAfter <= now, 'time-range in the future time', - ValidationErrors.NotInTimeRange) + ValidationErrors.NotInTimeRange, + ); - console.log('until', res.returnInfo.validUntil, 'now=', now) - requireCond(res.returnInfo.validUntil == null || res.returnInfo.validUntil >= now, + console.log('until', res.returnInfo.validUntil, 'now=', now); + requireCond( + res.returnInfo.validUntil == null || res.returnInfo.validUntil >= now, 'already expired', - ValidationErrors.NotInTimeRange) + ValidationErrors.NotInTimeRange, + ); - requireCond(res.returnInfo.validUntil == null || res.returnInfo.validUntil > now + VALID_UNTIL_FUTURE_SECONDS, + requireCond( + res.returnInfo.validUntil == null || + res.returnInfo.validUntil > now + VALID_UNTIL_FUTURE_SECONDS, 'expires too soon', - ValidationErrors.NotInTimeRange) + ValidationErrors.NotInTimeRange, + ); - requireCond(res.aggregatorInfo == null, + requireCond( + res.aggregatorInfo == null, 'Currently not supporting aggregator', - ValidationErrors.UnsupportedSignatureAggregator) + ValidationErrors.UnsupportedSignatureAggregator, + ); return { ...res, referencedContracts: codeHashes, - storageMap - } + storageMap, + }; } - async getCodeHashes (addresses: string[]): Promise { + async getCodeHashes(addresses: string[]): Promise { const { hash } = await runContractScript( this.entryPoint.provider, new CodeHashGetter__factory(), - [addresses] - ) + [addresses], + ); return { hash, - addresses - } + addresses, + }; } /** @@ -258,44 +327,82 @@ export class ValidationManager { * @param requireSignature * @param requireGasParams */ - validateInputParameters (userOp: UserOperation, entryPointInput: string, requireSignature = true, requireGasParams = true): void { - requireCond(entryPointInput != null, 'No entryPoint param', ValidationErrors.InvalidFields) - requireCond(entryPointInput.toLowerCase() === this.entryPoint.address.toLowerCase(), + validateInputParameters( + userOp: UserOperation, + entryPointInput: string, + requireSignature = true, + requireGasParams = true, + ): void { + requireCond( + entryPointInput != null, + 'No entryPoint param', + ValidationErrors.InvalidFields, + ); + requireCond( + entryPointInput.toLowerCase() === this.entryPoint.address.toLowerCase(), `The EntryPoint at "${entryPointInput}" is not supported. This bundler uses ${this.entryPoint.address}`, - ValidationErrors.InvalidFields) + ValidationErrors.InvalidFields, + ); // minimal sanity check: userOp exists, and all members are hex - requireCond(userOp != null, 'No UserOperation param', ValidationErrors.InvalidFields) - - const fields = ['sender', 'nonce', 'initCode', 'callData', 'paymasterAndData'] + requireCond( + userOp != null, + 'No UserOperation param', + ValidationErrors.InvalidFields, + ); + + const fields = [ + 'sender', + 'nonce', + 'initCode', + 'callData', + 'paymasterAndData', + ]; if (requireSignature) { - fields.push('signature') + fields.push('signature'); } if (requireGasParams) { - fields.push('preVerificationGas', 'verificationGasLimit', 'callGasLimit', 'maxFeePerGas', 'maxPriorityFeePerGas') + fields.push( + 'preVerificationGas', + 'verificationGasLimit', + 'callGasLimit', + 'maxFeePerGas', + 'maxPriorityFeePerGas', + ); } - fields.forEach(key => { - const value: string = (userOp as any)[key]?.toString() - requireCond(value != null, - 'Missing userOp field: ' + key + ' ' + JSON.stringify(userOp), - ValidationErrors.InvalidFields) - requireCond(value.match(HEX_REGEX) != null, + fields.forEach((key) => { + const value: string = (userOp as any)[key]?.toString(); + requireCond( + value != null, + `Missing userOp field: ${key} ${JSON.stringify(userOp)}`, + ValidationErrors.InvalidFields, + ); + requireCond( + value.match(HEX_REGEX) != null, `Invalid hex value for property ${key}:${value} in UserOp`, - ValidationErrors.InvalidFields) - }) + ValidationErrors.InvalidFields, + ); + }); - requireCond(userOp.paymasterAndData.length === 2 || userOp.paymasterAndData.length >= 42, + requireCond( + userOp.paymasterAndData.length === 2 || + userOp.paymasterAndData.length >= 42, 'paymasterAndData: must contain at least an address', - ValidationErrors.InvalidFields) + ValidationErrors.InvalidFields, + ); // syntactically, initCode can be only the deployer address. but in reality, it must have calldata to uniquely identify the account - requireCond(userOp.initCode.length === 2 || userOp.initCode.length >= 42, + requireCond( + userOp.initCode.length === 2 || userOp.initCode.length >= 42, 'initCode: must contain at least an address', - ValidationErrors.InvalidFields) + ValidationErrors.InvalidFields, + ); - const calcPreVerificationGas1 = calcPreVerificationGas(userOp) - requireCond(userOp.preVerificationGas >= calcPreVerificationGas1, + const calcPreVerificationGas1 = calcPreVerificationGas(userOp); + requireCond( + userOp.preVerificationGas >= calcPreVerificationGas1, `preVerificationGas too low: expected at least ${calcPreVerificationGas1}`, - ValidationErrors.InvalidFields) + ValidationErrors.InvalidFields, + ); } } diff --git a/src/validation-manager/index.ts b/src/validation-manager/index.ts index f5ba485..fda13b1 100644 --- a/src/validation-manager/index.ts +++ b/src/validation-manager/index.ts @@ -1,40 +1,52 @@ -import { JsonRpcProvider } from '@ethersproject/providers' +import type { JsonRpcProvider } from '@ethersproject/providers'; -import { AddressZero, UserOperation } from '../utils' -import { IEntryPoint__factory } from '../contract-types' +import { bundlerCollectorTracer } from './BundlerCollectorTracer'; +import { debug_traceCall } from './GethTracer'; +import type { ValidateUserOpResult } from './ValidationManager'; +import { ValidationManager } from './ValidationManager'; +import { IEntryPoint__factory } from '../contract-types'; +import { AddressZero } from '../utils'; +import type { UserOperation } from '../utils'; -import { bundlerCollectorTracer } from './BundlerCollectorTracer' -import { debug_traceCall } from './GethTracer' -import { ValidateUserOpResult, ValidationManager } from './ValidationManager' +export * from './ValidationManager'; -export * from './ValidationManager' - -export async function supportsDebugTraceCall (provider: JsonRpcProvider): Promise { - const p = provider.send as any +/** + * + * @param provider + */ +export async function supportsDebugTraceCall( + provider: JsonRpcProvider, +): Promise { + const p = provider.send as any; if (p._clientVersion == null) { - p._clientVersion = await provider.send('web3_clientVersion', []) + p._clientVersion = await provider.send('web3_clientVersion', []); } // make sure we can trace a call. - const ret = await debug_traceCall(provider, + const ret = await debug_traceCall( + provider, { from: AddressZero, to: AddressZero, data: '0x' }, - { tracer: bundlerCollectorTracer }).catch(e => e) - return ret.logs != null + { tracer: bundlerCollectorTracer }, + ).catch((e) => e); + return ret.logs != null; } -export async function checkRulesViolations ( +/** + * + * @param provider + * @param userOperation + * @param entryPointAddress + */ +export async function checkRulesViolations( provider: JsonRpcProvider, userOperation: UserOperation, - entryPointAddress: string + entryPointAddress: string, ): Promise { - const supportsTrace = await supportsDebugTraceCall(provider) + const supportsTrace = await supportsDebugTraceCall(provider); if (!supportsTrace) { - throw new Error('This provider does not support stack tracing') + throw new Error('This provider does not support stack tracing'); } - const entryPoint = IEntryPoint__factory.connect(entryPointAddress, provider) - const validationManager = new ValidationManager( - entryPoint, - false - ) - return await validationManager.validateUserOp(userOperation) + const entryPoint = IEntryPoint__factory.connect(entryPointAddress, provider); + const validationManager = new ValidationManager(entryPoint, false); + return await validationManager.validateUserOp(userOperation); } diff --git a/tsconfig.json b/tsconfig.json index 58db1d3..4a9e1dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.packages.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src", + "rootDir": "src" }, "include": ["src/**/*"], "exclude": ["**/*.test.ts", "src/bundler/test"] diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 0dabd86..cd4fae1 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -14,7 +14,6 @@ "declaration": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "sourceMap": true, - }, - "include": ["src/**/*"], + "sourceMap": true + } } diff --git a/yarn.lock b/yarn.lock index 17e6222..bb153c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -242,9 +242,16 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.0": - version: 2.1.0 - resolution: "@eslint/eslintrc@npm:2.1.0" +"@eslint-community/regexpp@npm:^4.6.1": + version: 4.10.0 + resolution: "@eslint-community/regexpp@npm:4.10.0" + checksum: 2a6e345429ea8382aaaf3a61f865cae16ed44d31ca917910033c02dc00d505d939f10b81e079fa14d43b51499c640138e153b7e40743c4c094d9df97d4e56f7b + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" dependencies: ajv: ^6.12.4 debug: ^4.3.2 @@ -255,14 +262,14 @@ __metadata: js-yaml: ^4.1.0 minimatch: ^3.1.2 strip-json-comments: ^3.1.1 - checksum: d5ed0adbe23f6571d8c9bb0ca6edf7618dc6aed4046aa56df7139f65ae7b578874e0d9c796df784c25bda648ceb754b6320277d828c8b004876d7443b8dc018c + checksum: 10957c7592b20ca0089262d8c2a8accbad14b4f6507e35416c32ee6b4dbf9cad67dfb77096bbd405405e9ada2b107f3797fe94362e1c55e0b09d6e90dd149127 languageName: node linkType: hard -"@eslint/js@npm:8.44.0": - version: 8.44.0 - resolution: "@eslint/js@npm:8.44.0" - checksum: fc539583226a28f5677356e9f00d2789c34253f076643d2e32888250e509a4e13aafe0880cb2425139051de0f3a48d25bfc5afa96b7304f203b706c17340e3cf +"@eslint/js@npm:8.56.0": + version: 8.56.0 + resolution: "@eslint/js@npm:8.56.0" + checksum: 5804130574ef810207bdf321c265437814e7a26f4e6fac9b496de3206afd52f533e09ec002a3be06cd9adcc9da63e727f1883938e663c4e4751c007d5b58e539 languageName: node linkType: hard @@ -715,14 +722,14 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.10": - version: 0.11.10 - resolution: "@humanwhocodes/config-array@npm:0.11.10" +"@humanwhocodes/config-array@npm:^0.11.13": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" dependencies: - "@humanwhocodes/object-schema": ^1.2.1 - debug: ^4.1.1 + "@humanwhocodes/object-schema": ^2.0.2 + debug: ^4.3.1 minimatch: ^3.0.5 - checksum: 1b1302e2403d0e35bc43e66d67a2b36b0ad1119efc704b5faff68c41f791a052355b010fb2d27ef022670f550de24cd6d08d5ecf0821c16326b7dcd0ee5d5d8a + checksum: 861ccce9eaea5de19546653bccf75bf09fe878bc39c3aab00aeee2d2a0e654516adad38dd1098aab5e3af0145bbcbf3f309bdf4d964f8dab9dcd5834ae4c02f2 languageName: node linkType: hard @@ -733,10 +740,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^1.2.1": - version: 1.2.1 - resolution: "@humanwhocodes/object-schema@npm:1.2.1" - checksum: a824a1ec31591231e4bad5787641f59e9633827d0a2eaae131a288d33c9ef0290bd16fda8da6f7c0fcb014147865d12118df10db57f27f41e20da92369fcb3f1 +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.2 + resolution: "@humanwhocodes/object-schema@npm:2.0.2" + checksum: 2fc11503361b5fb4f14714c700c02a3f4c7c93e9acd6b87a29f62c522d90470f364d6161b03d1cc618b979f2ae02aed1106fd29d302695d8927e2fc8165ba8ee languageName: node linkType: hard @@ -963,8 +970,8 @@ __metadata: "@types/express": ^4.17.13 "@types/mocha": ^9.1.0 "@types/node": ^16.4.12 - "@typescript-eslint/eslint-plugin": ^5.33.0 - "@typescript-eslint/parser": ^5.33.0 + "@typescript-eslint/eslint-plugin": ^5.43.0 + "@typescript-eslint/parser": ^5.43.0 async-mutex: ^0.4.0 body-parser: ^1.20.0 chai: ^4.2.0 @@ -972,7 +979,7 @@ __metadata: cors: ^2.8.5 debug: ^4.3.4 depcheck: ^1.4.3 - eslint: ^8.21.0 + eslint: ^8.44.0 eslint-config-prettier: ^8.8.0 eslint-plugin-import: ^2.26.0 eslint-plugin-jest: ^27.2.2 @@ -2088,7 +2095,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.33.0": +"@typescript-eslint/eslint-plugin@npm:^5.43.0": version: 5.62.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0" dependencies: @@ -2112,7 +2119,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.33.0": +"@typescript-eslint/parser@npm:^5.43.0": version: 5.62.0 resolution: "@typescript-eslint/parser@npm:5.62.0" dependencies: @@ -2209,6 +2216,13 @@ __metadata: languageName: node linkType: hard +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524 + languageName: node + linkType: hard + "@vue/compiler-core@npm:3.3.4": version: 3.3.4 resolution: "@vue/compiler-core@npm:3.3.4" @@ -2424,7 +2438,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.0, ajv@npm:^6.12.3, ajv@npm:^6.12.4": +"ajv@npm:^6.12.3, ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -4623,13 +4637,13 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.2.0": - version: 7.2.1 - resolution: "eslint-scope@npm:7.2.1" +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" dependencies: esrecurse: ^4.3.0 estraverse: ^5.2.0 - checksum: dccda5c8909216f6261969b72c77b95e385f9086bed4bc09d8a6276df8439d8f986810fd9ac3bd02c94c0572cefc7fdbeae392c69df2e60712ab8263986522c5 + checksum: ec97dbf5fb04b94e8f4c5a91a7f0a6dd3c55e46bfc7bbcd0e3138c3a76977570e02ed89a1810c778dcd72072ff0e9621ba1379b4babe53921d71e2e4486fda3e languageName: node linkType: hard @@ -4674,26 +4688,34 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.21.0": - version: 8.45.0 - resolution: "eslint@npm:8.45.0" +"eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 36e9ef87fca698b6fd7ca5ca35d7b2b6eeaaf106572e2f7fd31c12d3bfdaccdb587bba6d3621067e5aece31c8c3a348b93922ab8f7b2cbc6aaab5e1d89040c60 + languageName: node + linkType: hard + +"eslint@npm:^8.44.0": + version: 8.56.0 + resolution: "eslint@npm:8.56.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 - "@eslint-community/regexpp": ^4.4.0 - "@eslint/eslintrc": ^2.1.0 - "@eslint/js": 8.44.0 - "@humanwhocodes/config-array": ^0.11.10 + "@eslint-community/regexpp": ^4.6.1 + "@eslint/eslintrc": ^2.1.4 + "@eslint/js": 8.56.0 + "@humanwhocodes/config-array": ^0.11.13 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 - ajv: ^6.10.0 + "@ungap/structured-clone": ^1.2.0 + ajv: ^6.12.4 chalk: ^4.0.0 cross-spawn: ^7.0.2 debug: ^4.3.2 doctrine: ^3.0.0 escape-string-regexp: ^4.0.0 - eslint-scope: ^7.2.0 - eslint-visitor-keys: ^3.4.1 - espree: ^9.6.0 + eslint-scope: ^7.2.2 + eslint-visitor-keys: ^3.4.3 + espree: ^9.6.1 esquery: ^1.4.2 esutils: ^2.0.2 fast-deep-equal: ^3.1.3 @@ -4717,11 +4739,11 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 3e6dcce5cc43c5e301662db88ee26d1d188b22c177b9f104d7eefd1191236980bd953b3670fe2fac287114b26d7c5420ab48407d7ea1c3a446d6313c000009da + checksum: 883436d1e809b4a25d9eb03d42f584b84c408dbac28b0019f6ea07b5177940bf3cca86208f749a6a1e0039b63e085ee47aca1236c30721e91f0deef5cc5a5136 languageName: node linkType: hard -"espree@npm:^9.6.0": +"espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" dependencies: