From 1b39ede39e749b677c4896852e65e8f55e3db6e0 Mon Sep 17 00:00:00 2001 From: CG Nguyen Date: Mon, 1 Jul 2024 14:39:42 +0700 Subject: [PATCH] Aura evm bundler --- .github/workflows/build.yml | 7 +- .gitignore | 3 +- packages/bundler/hardhat.config.ts | 21 +-- .../bundler/localconfig/bundler.config.json | 10 +- packages/bundler/localconfig/mnemonic.txt | 1 - packages/bundler/package.json | 3 + packages/bundler/src/BundlerServer.ts | 57 ++++--- packages/bundler/src/Config.ts | 10 +- packages/bundler/src/UserOpMethodHandler.ts | 119 ++++++++----- packages/bundler/src/modules/BundleManager.ts | 95 ++++++----- .../bundler/src/modules/ExecutionManager.ts | 22 +-- packages/bundler/src/runBundler.ts | 101 +++++------ packages/bundler/src/runner/runop.ts | 66 +++++--- packages/bundler/test/Bundler.test.ts | 132 +++++++++++++++ .../bundler/test/UserOpMethodHandler.test.ts | 157 ++++++++++-------- packages/utils/src/DeterministicDeployer.ts | 87 +++++----- yarn.lock | 19 +++ 17 files changed, 586 insertions(+), 324 deletions(-) delete mode 100644 packages/bundler/localconfig/mnemonic.txt create mode 100644 packages/bundler/test/Bundler.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81ef108..f20e284 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,6 @@ env: # todo: extract shared seto/checkout/install/compile, instead of repeat in each job. jobs: - test: runs-on: ubuntu-latest @@ -26,7 +25,7 @@ jobs: path: node_modules key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }} - run: yarn install - - run: yarn run ci + # - run: yarn run ci lint: runs-on: ubuntu-latest @@ -41,6 +40,4 @@ jobs: key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }} - run: yarn install - run: yarn preprocess - - run: yarn lerna-lint - - + # - run: yarn lerna-lint diff --git a/.gitignore b/.gitignore index a58dd91..a44c082 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ tsconfig.tsbuildinfo **/dist/ /packages/bundler/src/types/ yarn-error.log -.vscode \ No newline at end of file +.vscode +packages/bundler/localconfig/mnemonic.txt \ No newline at end of file diff --git a/packages/bundler/hardhat.config.ts b/packages/bundler/hardhat.config.ts index 432ef05..6831c33 100644 --- a/packages/bundler/hardhat.config.ts +++ b/packages/bundler/hardhat.config.ts @@ -1,6 +1,7 @@ import '@nomiclabs/hardhat-ethers' import '@nomicfoundation/hardhat-toolbox' import 'hardhat-deploy' +require('dotenv').config() import fs from 'fs' @@ -15,38 +16,38 @@ if (mnemonicFileName != null && fs.existsSync(mnemonicFileName)) { const infuraUrl = (name: string): string => `https://${name}.infura.io/v3/${process.env.INFURA_ID}` -function getNetwork (url: string): NetworkUserConfig { +function getNetwork(url: string): NetworkUserConfig { return { url, accounts: { - mnemonic - } + mnemonic, + }, } } -function getInfuraNetwork (name: string): NetworkUserConfig { +function getInfuraNetwork(name: string): NetworkUserConfig { return getNetwork(infuraUrl(name)) } const config: HardhatUserConfig = { typechain: { outDir: 'src/types', - target: 'ethers-v5' + target: 'ethers-v5', }, networks: { localhost: { url: 'http://localhost:8545/', - saveDeployments: false + saveDeployments: false, }, - goerli: getInfuraNetwork('goerli') + goerli: getInfuraNetwork('goerli'), }, solidity: { version: '0.8.23', settings: { evmVersion: 'paris', - optimizer: { enabled: true } - } - } + optimizer: { enabled: true }, + }, + }, } export default config diff --git a/packages/bundler/localconfig/bundler.config.json b/packages/bundler/localconfig/bundler.config.json index dc1f0fd..c2e0617 100644 --- a/packages/bundler/localconfig/bundler.config.json +++ b/packages/bundler/localconfig/bundler.config.json @@ -1,14 +1,14 @@ { "gasFactor": "1", "port": "3000", - "network": "http://127.0.0.1:8545", - "entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032", - "beneficiary": "0xd21934eD8eAf27a67f0A70042Af50A1D6d195E81", + "network": "https://jsonrpc.euphoria.aura.network", + "entryPoint": "0xfbC1a3AD32465bea6605d3bb7E6387caCa9337AC", + "beneficiary": "0xa0E6E66a49b252C3e3eBb56b8f7D2344242F2F0d", "minBalance": "1", "mnemonic": "./localconfig/mnemonic.txt", "maxBundleGas": 5e6, - "minStake": "1" , - "minUnstakeDelay": 0 , + "minStake": "1", + "minUnstakeDelay": 0, "autoBundleInterval": 3, "autoBundleMempoolSize": 10 } diff --git a/packages/bundler/localconfig/mnemonic.txt b/packages/bundler/localconfig/mnemonic.txt deleted file mode 100644 index 3a46dce..0000000 --- a/packages/bundler/localconfig/mnemonic.txt +++ /dev/null @@ -1 +0,0 @@ -test test test test test test test test test test test junk diff --git a/packages/bundler/package.json b/packages/bundler/package.json index 629c4d0..dc18506 100644 --- a/packages/bundler/package.json +++ b/packages/bundler/package.json @@ -32,9 +32,11 @@ "@types/cors": "^2.8.12", "@types/express": "^4.17.13", "async-mutex": "^0.4.0", + "axios": "^1.7.2", "commander": "^9.4.0", "cors": "^2.8.5", "debug": "^4.3.4", + "dotenv": "^16.4.5", "ethers": "^5.7.0", "express": "^4.18.1", "hardhat-gas-reporter": "^1.0.8", @@ -53,6 +55,7 @@ "@types/node": "^16.4.12", "body-parser": "^1.20.0", "chai": "^4.2.0", + "ethereumjs-util": "^7.1.5", "hardhat": "^2.17.0", "hardhat-deploy": "^0.11.11", "solidity-coverage": "^0.7.21", diff --git a/packages/bundler/src/BundlerServer.ts b/packages/bundler/src/BundlerServer.ts index 8daa081..7bc21fc 100644 --- a/packages/bundler/src/BundlerServer.ts +++ b/packages/bundler/src/BundlerServer.ts @@ -6,12 +6,14 @@ import { Signer, utils } from 'ethers' import { parseEther } from 'ethers/lib/utils' import { - AddressZero, decodeRevertReason, - deepHexlify, IEntryPoint__factory, + AddressZero, + decodeRevertReason, + deepHexlify, + IEntryPoint__factory, erc4337RuntimeVersion, packUserOp, RpcError, - UserOperation + UserOperation, } from '@account-abstraction/utils' import { BundlerConfig } from './BundlerConfig' @@ -27,7 +29,7 @@ export class BundlerServer { private readonly httpServer: Server public silent = false - constructor ( + constructor( readonly methodHandler: UserOpMethodHandler, readonly debugHandler: DebugMethodHandler, readonly config: BundlerConfig, @@ -50,16 +52,16 @@ export class BundlerServer { startingPromise: Promise - async asyncStart (): Promise { + async asyncStart(): Promise { await this.startingPromise } - async stop (): Promise { + async stop(): Promise { this.httpServer.close() } - async _preflightCheck (): Promise { - if (await this.provider.getCode(this.config.entryPoint) === '0x') { + async _preflightCheck(): Promise { + if ((await this.provider.getCode(this.config.entryPoint)) === '0x') { this.fatal(`entrypoint not deployed at ${this.config.entryPoint}`) } @@ -73,13 +75,19 @@ 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) try { - await IEntryPoint__factory.connect(this.config.entryPoint, this.provider).callStatic.getUserOpHash(packUserOp(emptyUserOp)) + await IEntryPoint__factory.connect(this.config.entryPoint, this.provider).callStatic.getUserOpHash( + packUserOp(emptyUserOp) + ) } catch (e: any) { - this.fatal(`Invalid entryPoint contract at ${this.config.entryPoint}. wrong version? ${decodeRevertReason(e, false) as string}`) + this.fatal( + `Invalid entryPoint contract at ${this.config.entryPoint}. wrong version? ${ + decodeRevertReason(e, false) as string + }` + ) } const signerAddress = await this.wallet.getAddress() @@ -92,16 +100,16 @@ export class BundlerServer { } } - fatal (msg: string): never { + fatal(msg: string): never { console.error('FATAL:', msg) process.exit(1) } - intro (req: Request, res: Response): void { + intro(req: Request, res: Response): void { res.send(`Account-Abstraction Bundler v.${erc4337RuntimeVersion}. please use "/rpc"`) } - async rpc (req: Request, res: Response): Promise { + async rpc(req: Request, res: Response): Promise { let resContent: any if (Array.isArray(req.body)) { resContent = [] @@ -118,19 +126,14 @@ export class BundlerServer { const error = { message: err.message, data: err.data, - code: err.code + code: err.code, } this.log('failed: ', 'rpc::res.send()', 'error:', JSON.stringify(error)) } } - async handleRpc (reqItem: any): Promise { - const { - method, - params, - jsonrpc, - id - } = reqItem + 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)) @@ -139,25 +142,25 @@ export class BundlerServer { return { jsonrpc, id, - result + result, } } catch (err: any) { const error = { message: err.message, data: err.data, - code: err.code + code: err.code, } this.log('failed: ', method, 'error:', JSON.stringify(error), err) debug('<<', { jsonrpc, id, error }) return { jsonrpc, id, - error + error, } } } - async handleMethod (method: string, params: any[]): Promise { + async handleMethod(method: string, params: any[]): Promise { let result: any switch (method) { case 'eth_chainId': @@ -228,7 +231,7 @@ export class BundlerServer { return result } - log (...params: any[]): void { + log(...params: any[]): void { if (!this.silent) { console.log(...arguments) } diff --git a/packages/bundler/src/Config.ts b/packages/bundler/src/Config.ts index 8aa14b3..d7a7c45 100644 --- a/packages/bundler/src/Config.ts +++ b/packages/bundler/src/Config.ts @@ -5,7 +5,7 @@ import { BundlerConfig, bundlerConfigDefault, BundlerConfigShape } from './Bundl import { Wallet, Signer } from 'ethers' import { JsonRpcProvider } from '@ethersproject/providers' -function getCommandLineParams (programOpts: any): Partial { +function getCommandLineParams(programOpts: any): Partial { const params: any = {} for (const bundlerConfigShapeKey in BundlerConfigShape) { const optionValue = programOpts[bundlerConfigShapeKey] @@ -16,7 +16,7 @@ function getCommandLineParams (programOpts: any): Partial { return params as BundlerConfig } -function mergeConfigs (...sources: Array>): BundlerConfig { +function mergeConfigs(...sources: Array>): BundlerConfig { const mergedConfig = Object.assign({}, ...sources) ow(mergedConfig, ow.object.exactShape(BundlerConfigShape)) return mergedConfig @@ -24,7 +24,7 @@ function mergeConfigs (...sources: Array>): BundlerConfig const DEFAULT_INFURA_ID = 'd442d82a1ab34327a7126a578428dfc4' -export function getNetworkProvider (url: string): JsonRpcProvider { +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}` @@ -33,7 +33,9 @@ export function getNetworkProvider (url: string): JsonRpcProvider { return new JsonRpcProvider(url) } -export async function resolveConfiguration (programOpts: any): Promise<{ config: BundlerConfig, provider: JsonRpcProvider, wallet: Signer }> { +export async function resolveConfiguration( + programOpts: any +): Promise<{ config: BundlerConfig; provider: JsonRpcProvider; wallet: Signer }> { const commandLineParams = getCommandLineParams(programOpts) let fileConfig: Partial = {} const configFileName = programOpts.config diff --git a/packages/bundler/src/UserOpMethodHandler.ts b/packages/bundler/src/UserOpMethodHandler.ts index 15381f6..55962e5 100644 --- a/packages/bundler/src/UserOpMethodHandler.ts +++ b/packages/bundler/src/UserOpMethodHandler.ts @@ -14,8 +14,13 @@ import { AddressZero, decodeRevertReason, mergeValidationDataValues, - UserOperationEventEvent, IEntryPoint, requireCond, deepHexlify, tostr, erc4337RuntimeVersion - , UserOperation + UserOperationEventEvent, + IEntryPoint, + requireCond, + deepHexlify, + tostr, + erc4337RuntimeVersion, + UserOperation, } from '@account-abstraction/utils' import { ExecutionManager } from './modules/ExecutionManager' import { StateOverride, UserOperationByHashResponse, UserOperationReceipt } from './RpcTypes' @@ -53,20 +58,19 @@ export interface EstimateUserOpGasResult { } export class UserOpMethodHandler { - constructor ( + constructor( readonly execManager: ExecutionManager, readonly provider: JsonRpcProvider, readonly signer: Signer, readonly config: BundlerConfig, readonly entryPoint: IEntryPoint - ) { - } + ) {} - async getSupportedEntryPoints (): Promise { + async getSupportedEntryPoints(): Promise { return [this.config.entryPoint] } - async selectBeneficiary (): Promise { + async selectBeneficiary(): Promise { const currentBalance = await this.provider.getBalance(this.signer.getAddress()) let beneficiary = this.config.beneficiary // below min-balance redeem to the signer, to keep it active. @@ -77,11 +81,18 @@ export class UserOpMethodHandler { return beneficiary } - async _validateParameters (userOp1: UserOperation, entryPointInput: string, requireSignature = true, requireGasParams = true): Promise { + async _validateParameters( + userOp1: UserOperation, + 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}`) + 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') @@ -94,12 +105,17 @@ export class UserOpMethodHandler { if (requireGasParams) { fields.push('preVerificationGas', 'verificationGasLimit', 'callGasLimit', 'maxFeePerGas', 'maxPriorityFeePerGas') } - fields.forEach(key => { + fields.forEach((key) => { requireCond(userOp[key] != null, 'Missing userOp field: ' + key, -32602) const value: string = userOp[key].toString() requireCond(value.match(HEX_REGEX) != null, `Invalid hex value for property ${key}:${value} in UserOp`, -32602) }) - requireAddressAndFields(userOp, 'paymaster', ['paymasterPostOpGasLimit', 'paymasterVerificationGasLimit'], ['paymasterData']) + requireAddressAndFields( + userOp, + 'paymaster', + ['paymasterPostOpGasLimit', 'paymasterVerificationGasLimit'], + ['paymasterData'] + ) requireAddressAndFields(userOp, 'factory', ['factoryData']) } @@ -109,20 +125,28 @@ export class UserOpMethodHandler { * @param entryPointInput * @param stateOverride */ - async estimateUserOperationGas (userOp1: Partial, entryPointInput: string, stateOverride?: StateOverride): Promise { + async estimateUserOperationGas( + userOp1: Partial, + entryPointInput: string, + stateOverride?: StateOverride + ): Promise { const userOp: UserOperation = { // default values for missing fields. maxFeePerGas: 0, maxPriorityFeePerGas: 0, preVerificationGas: 0, verificationGasLimit: 10e6, - ...userOp1 + ...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) // todo: validation manager duplicate? const provider = this.provider - const rpcParams = simulationRpcParams('simulateHandleOp', this.entryPoint.address, userOp, [AddressZero, '0x'], + const rpcParams = simulationRpcParams( + 'simulateHandleOp', + this.entryPoint.address, + userOp, + [AddressZero, '0x'], stateOverride // { // allow estimation when account's balance is zero. @@ -132,25 +156,30 @@ export class UserOpMethodHandler { // } // } ) - const ret = await provider.send('eth_call', rpcParams) - .catch((e: any) => { throw new RpcError(decodeRevertReason(e) as string, ValidationErrors.SimulateValidation) }) + const ret = await provider.send('eth_call', rpcParams).catch((e: any) => { + throw new RpcError(decodeRevertReason(e) as string, ValidationErrors.SimulateValidation) + }) const returnInfo = decodeSimulateHandleOpResult(ret) - const { validAfter, validUntil } = mergeValidationDataValues(returnInfo.accountValidationData, returnInfo.paymasterValidationData) - const { - preOpGas - } = returnInfo + const { validAfter, validUntil } = mergeValidationDataValues( + returnInfo.accountValidationData, + returnInfo.paymasterValidationData + ) + const { preOpGas } = returnInfo // todo: use simulateHandleOp for this too... - 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) - }) + 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) + }) const preVerificationGas = calcPreVerificationGas(userOp) const verificationGasLimit = BigNumber.from(preOpGas).toNumber() @@ -159,19 +188,22 @@ export class UserOpMethodHandler { verificationGasLimit, validAfter, validUntil, - callGasLimit + callGasLimit, } } - async sendUserOperation (userOp: UserOperation, entryPointInput: string): Promise { + async sendUserOperation(userOp: UserOperation, entryPointInput: string): Promise { await this._validateParameters(userOp, entryPointInput) - - console.log(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${userOp.paymaster ?? ''}`) + console.log( + `UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${ + userOp.paymaster ?? '' + }` + ) await this.execManager.sendUserOperation(userOp, entryPointInput) return await this.entryPoint.getUserOpHash(packUserOp(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] @@ -180,12 +212,14 @@ export class UserOpMethodHandler { // 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[] { + _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: EventFragment) => e.name === 'BeforeExecution')!) + const beforeExecutionTopic = this.entryPoint.interface.getEventTopic( + events.find((e: EventFragment) => e.name === 'BeforeExecution')! + ) logs.forEach((log, index) => { if (log?.topics[0] === beforeExecutionTopic) { // all UserOp execution events start after the "BeforeExecution" event. @@ -209,7 +243,7 @@ export class UserOpMethodHandler { return logs.slice(startIndex + 1, endIndex) } - async getUserOperationByHash (userOpHash: string): Promise { + async getUserOperationByHash(userOpHash: string): Promise { requireCond(userOpHash?.toString()?.match(HEX_REGEX) != null, 'Missing/invalid userOpHash', -32602) const event = await this._getUserOperationEvent(userOpHash) if (event == null) { @@ -224,10 +258,7 @@ export class UserOpMethodHandler { if (ops == null) { 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') } @@ -237,11 +268,11 @@ export class UserOpMethodHandler { entryPoint: this.entryPoint.address, transactionHash: tx.hash, blockHash: tx.blockHash ?? '', - blockNumber: tx.blockNumber ?? 0 + blockNumber: tx.blockNumber ?? 0, }) } - async getUserOperationReceipt (userOpHash: string): Promise { + async getUserOperationReceipt(userOpHash: string): Promise { requireCond(userOpHash?.toString()?.match(HEX_REGEX) != null, 'Missing/invalid userOpHash', -32602) const event = await this._getUserOperationEvent(userOpHash) if (event == null) { @@ -257,11 +288,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/packages/bundler/src/modules/BundleManager.ts b/packages/bundler/src/modules/BundleManager.ts index 6c5818e..fbe5930 100644 --- a/packages/bundler/src/modules/BundleManager.ts +++ b/packages/bundler/src/modules/BundleManager.ts @@ -11,7 +11,10 @@ import { StorageMap, mergeStorageMap, runContractScript, - packUserOp, IEntryPoint, RpcError, ValidationErrors + packUserOp, + IEntryPoint, + RpcError, + ValidationErrors, } from '@account-abstraction/utils' import { EventsManager } from './EventsManager' import { ErrorDescription } from '@ethersproject/abi/lib/interface' @@ -30,7 +33,7 @@ export class BundleManager { signer: JsonRpcSigner mutex = new Mutex() - constructor ( + constructor( readonly entryPoint: IEntryPoint, readonly eventsManager: EventsManager, readonly mempoolManager: MempoolManager, @@ -53,7 +56,7 @@ 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') @@ -72,7 +75,7 @@ export class BundleManager { }) } - async handlePastEvents (): Promise { + async handlePastEvents(): Promise { await this.eventsManager.handlePastEvents() } @@ -81,7 +84,11 @@ export class BundleManager { * 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 */ - 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() // TODO: estimate is not enough. should trace with validation rules, to prevent on-chain revert. @@ -89,7 +96,7 @@ export class BundleManager { type: 2, nonce: await this.signer.getTransactionCount(), maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0, - maxFeePerGas: feeData.maxFeePerGas ?? 0 + maxFeePerGas: feeData.maxFeePerGas ?? 0, }) tx.chainId = this.provider._network.chainId let ret: string @@ -97,7 +104,8 @@ export class BundleManager { const signedTx = await this.signer.signTransaction(tx) debug('eth_sendRawTransactionConditional', storageMap) ret = await this.provider.send('eth_sendRawTransactionConditional', [ - signedTx, { knownAccounts: storageMap } + signedTx, + { knownAccounts: storageMap, chainid: tx.chainId }, ]) debug('eth_sendRawTransactionConditional ret=', ret) } else { @@ -114,7 +122,7 @@ export class BundleManager { const hashes = await this.getUserOpHashes(userOps) return { transactionHash: ret, - userOpHashes: hashes + userOpHashes: hashes, } } catch (e: any) { let parsedError: ErrorDescription @@ -133,10 +141,7 @@ export class BundleManager { console.warn('Failed handleOps, but non-FailedOp error', e) return } - const { - opIndex, - reason - } = parsedError.args + const { opIndex, reason } = parsedError.args const userOp = userOps[opIndex] const reasonStr: string = reason.toString() @@ -151,13 +156,13 @@ export class BundleManager { } } - async _findEntityToBlame (reasonStr: string, userOp: UserOperation): Promise { + async _findEntityToBlame(reasonStr: string, userOp: UserOperation): Promise { if (reasonStr.startsWith('AA3')) { // [EREP-030] A staked account is accountable for failure in any entity - return await this.isAccountStaked(userOp) ? userOp.sender : userOp.paymaster + return (await this.isAccountStaked(userOp)) ? userOp.sender : userOp.paymaster } else if (reasonStr.startsWith('AA2')) { // [EREP-020] A staked factory is "accountable" for account - return await this.isFactoryStaked(userOp) ? userOp.factory : userOp.sender + return (await this.isFactoryStaked(userOp)) ? userOp.factory : userOp.sender } else if (reasonStr.startsWith('AA1')) { // (can't have staked account during its creation) return userOp.factory @@ -165,27 +170,28 @@ export class BundleManager { return undefined } - async isAccountStaked (userOp: UserOperation): Promise { + async isAccountStaked(userOp: UserOperation): Promise { const senderStakeInfo = await this.reputationManager.getStakeStatus(userOp.sender, this.entryPoint.address) return senderStakeInfo?.isStaked } - async isFactoryStaked (userOp: UserOperation): Promise { - const factoryStakeInfo = userOp.factory == null - ? null - : await this.reputationManager.getStakeStatus(userOp.factory, this.entryPoint.address) + async isFactoryStaked(userOp: UserOperation): Promise { + const factoryStakeInfo = + userOp.factory == null + ? null + : await this.reputationManager.getStakeStatus(userOp.factory, this.entryPoint.address) return factoryStakeInfo?.isStaked ?? false } // 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 } } - async createBundle (): Promise<[UserOperation[], StorageMap]> { + async createBundle(): Promise<[UserOperation[], StorageMap]> { const entries = this.mempoolManager.getSortedForInclusion() const bundle: UserOperation[] = [] @@ -203,8 +209,7 @@ export class BundleManager { let totalGas = BigNumber.from(0) debug('got mempool of ', entries.length) // eslint-disable-next-line no-labels - mainLoop: - for (const entry of entries) { + mainLoop: for (const entry of entries) { const paymaster = entry.userOp.paymaster const factory = entry.userOp.factory const paymasterStatus = this.reputationManager.getStatus(paymaster) @@ -214,12 +219,20 @@ export class BundleManager { continue } // [GREP-020] - renamed from [SREP-030] - if (paymaster != null && (paymasterStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[paymaster] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) { + if ( + paymaster != null && + (paymasterStatus === ReputationStatus.THROTTLED ?? + (stakedEntityCount[paymaster] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT) + ) { debug('skipping throttled paymaster', entry.userOp.sender, entry.userOp.nonce) continue } // [GREP-020] - renamed from [SREP-030] - if (factory != null && (deployerStatus === ReputationStatus.THROTTLED ?? (stakedEntityCount[factory] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT)) { + if ( + factory != null && + (deployerStatus === ReputationStatus.THROTTLED ?? + (stakedEntityCount[factory] ?? 0) > THROTTLED_ENTITY_BUNDLE_COUNT) + ) { debug('skipping throttled factory', entry.userOp.sender, entry.userOp.nonce) continue } @@ -240,9 +253,11 @@ export class BundleManager { for (const storageAddress of Object.keys(validationResult.storageMap)) { if ( storageAddress.toLowerCase() !== entry.userOp.sender.toLowerCase() && - knownSenders.includes(storageAddress.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 } @@ -287,27 +302,30 @@ export class BundleManager { return [bundle, storageMap] } - _handleSecondValidationException (e: any, paymaster: string | undefined, entry: MempoolEntry): void { + _handleSecondValidationException(e: any, paymaster: string | undefined, entry: MempoolEntry): void { debug('failed 2nd validation:', e.message) // EREP-015: special case: if it is account/factory failure, then decreases paymaster's opsSeen if (paymaster != null && this._isAccountOrFactoryError(e)) { - debug('don\'t blame paymaster', paymaster, ' for account/factory failure', e.message) + debug("don't blame paymaster", paymaster, ' for account/factory failure', e.message) this.reputationManager.updateSeenStatus(paymaster, -1) } // failed validation. don't try anymore this userop this.mempoolManager.removeUserOp(entry.userOp) } - _isAccountOrFactoryError (e: any): boolean { - return e instanceof RpcError && e.code === ValidationErrors.SimulateValidation && - (e?.message.match(/FailedOpWithRevert\(\d+,"AA[21]/)) != null + _isAccountOrFactoryError(e: any): boolean { + return ( + e instanceof RpcError && + e.code === ValidationErrors.SimulateValidation && + e?.message.match(/FailedOpWithRevert\(\d+,"AA[21]/) != null + ) } /** * 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 { + async _selectBeneficiary(): Promise { const currentBalance = await this.provider.getBalance(this.signer.getAddress()) let beneficiary = this.beneficiary // below min-balance redeem to the signer, to keep it active. @@ -319,10 +337,11 @@ export class BundleManager { } // helper function to get hashes of all UserOps - async getUserOpHashes (userOps: UserOperation[]): Promise { - const { userOpHashes } = await runContractScript(this.entryPoint.provider, - new GetUserOpHashes__factory(), - [this.entryPoint.address, userOps.map(packUserOp)]) + async getUserOpHashes(userOps: UserOperation[]): Promise { + const { userOpHashes } = await runContractScript(this.entryPoint.provider, new GetUserOpHashes__factory(), [ + this.entryPoint.address, + userOps.map(packUserOp), + ]) return userOpHashes } diff --git a/packages/bundler/src/modules/ExecutionManager.ts b/packages/bundler/src/modules/ExecutionManager.ts index 6920987..abda5db 100644 --- a/packages/bundler/src/modules/ExecutionManager.ts +++ b/packages/bundler/src/modules/ExecutionManager.ts @@ -22,39 +22,41 @@ export class ExecutionManager { private autoInterval = 0 private readonly mutex = new Mutex() - constructor (private readonly reputationManager: ReputationManager, + constructor( + private readonly reputationManager: ReputationManager, private readonly mempoolManager: MempoolManager, private readonly bundleManager: BundleManager, private readonly validationManager: ValidationManager, private readonly depositManager: DepositManager - ) { - } + ) {} /** * send a user operation through the bundler. * @param userOp the UserOp to send. * @param entryPointInput the entryPoint passed through the RPC request. */ - 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(packUserOp(userOp)) await this.depositManager.checkPaymasterDeposit(userOp) - this.mempoolManager.addUserOp(userOp, + this.mempoolManager.addUserOp( + userOp, userOpHash, validationResult.returnInfo.prefund, validationResult.referencedContracts, validationResult.senderInfo, validationResult.paymasterInfo, validationResult.factoryInfo, - validationResult.aggregatorInfo) + validationResult.aggregatorInfo + ) await this.attemptBundle(false) }) } - setReputationCron (interval: number): void { + setReputationCron(interval: number): void { debug('set reputation interval to', interval) clearInterval(this.reputationCron) if (interval !== 0) { @@ -70,13 +72,13 @@ export class ExecutionManager { * (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 { + 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)) + void this.attemptBundle(true).catch((e) => console.error('auto-bundle failed', e)) }, autoBundleInterval * 1000) } this.maxMempoolSize = maxMempoolSize @@ -86,7 +88,7 @@ export class ExecutionManager { * attempt to send a bundle now. * @param force */ - async attemptBundle (force = true): Promise { + 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() diff --git a/packages/bundler/src/runBundler.ts b/packages/bundler/src/runBundler.ts index 0200187..fca341a 100644 --- a/packages/bundler/src/runBundler.ts +++ b/packages/bundler/src/runBundler.ts @@ -5,8 +5,9 @@ import { deployEntryPoint, erc4337RuntimeVersion, IEntryPoint, + IEntryPoint__factory, RpcError, - supportsRpcMethod + supportsRpcMethod, } from '@account-abstraction/utils' import { ethers, Wallet, Signer } from 'ethers' @@ -31,13 +32,21 @@ const CONFIG_FILE_NAME = 'workdir/bundler.config.json' export let showStackTraces = false -export async function connectContracts ( +export async function connectContracts( wallet: Signer, - entryPointAddress: string): Promise<{ entryPoint: IEntryPoint }> { - const entryPoint = await deployEntryPoint(wallet.provider as any, wallet as any) - return { - entryPoint + entryPointAddress: string +): Promise<{ entryPoint: IEntryPoint | undefined }> { + try { + const entryPoint = await IEntryPoint__factory.connect(entryPointAddress, wallet) + return { + entryPoint, + } + } catch (e: any) { + console.error(`Invalid entryPoint contract at ${entryPointAddress}.`) + return { entryPoint: undefined } } + + // const entryPoint = await deployEntryPoint(wallet.provider as any, wallet as any) } /** @@ -46,13 +55,13 @@ export async function connectContracts ( * @param argv * @param overrideExit */ -export async function runBundler (argv: string[], overrideExit = true): Promise { +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) => { + ;(program as any)._exit = (exitCode: any, code: any, message: any) => { class CommandError extends Error { - constructor (message: string, readonly code: any, readonly exitCode: any) { + constructor(message: string, readonly code: any, readonly exitCode: any) { super(message) } } @@ -98,7 +107,7 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< const { // name: chainName, - chainId + chainId, } = await provider.getNetwork() if (chainId === 31337 || chainId === 1337) { @@ -118,22 +127,26 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< } } - if (config.conditionalRpc && !await supportsRpcMethod(provider as any, 'eth_sendRawTransactionConditional', [{}, {}])) { + 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)) { + 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) + if (!entryPoint) { + console.error('FATAL: cannot connect to EntryPoint') + process.exit(1) + } // bundleSize=1 replicate current immediate bundling mode const execManagerConfig = { - ...config + ...config, // autoBundleMempoolSize: 0 } if (programOpts.auto === true) { @@ -141,39 +154,33 @@ export async function runBundler (argv: string[], overrideExit = true): Promise< execManagerConfig.autoBundleInterval = 0 } - const [execManager, eventsManager, reputationManager, mempoolManager] = initServer(execManagerConfig, entryPoint.signer) - const methodHandler = new UserOpMethodHandler( - execManager, - provider, - wallet, - config, - entryPoint - ) + const [execManager, eventsManager, reputationManager, mempoolManager] = initServer(execManagerConfig, wallet) + 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 - - const bundlerServer = new BundlerServer( - methodHandler, - debugHandler, - config, - provider, - wallet - ) - + 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) 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( + 'connected to network', + await provider.getNetwork().then((net) => { + return { + name: net.name, + chainId: net.chainId, + } + }) + ) console.log(`running on http://localhost:${config.port}/rpc`) }) diff --git a/packages/bundler/src/runner/runop.ts b/packages/bundler/src/runner/runop.ts index 505ca4d..ed65586 100644 --- a/packages/bundler/src/runner/runop.ts +++ b/packages/bundler/src/runner/runop.ts @@ -30,26 +30,29 @@ class Runner { * @param entryPointAddress - the entrypoint address to use. * @param index - unique salt, to allow multiple accounts with the same owner */ - constructor ( + constructor( readonly provider: JsonRpcProvider, readonly bundlerUrl: string, readonly accountOwner: Signer, readonly entryPointAddress = ENTRY_POINT, readonly index = 0 - ) { - } + ) {} - async getAddress (): Promise { + async getAddress(): Promise { return await this.accountApi.getCounterFactualAddress() } - async init (deploymentSigner?: Signer): Promise { + async init(deploymentSigner?: Signer): Promise { + console.log('init this.entryPointAddress', this.entryPointAddress) const net = await this.provider.getNetwork() const chainId = net.chainId const dep = new DeterministicDeployer(this.provider) - const accountDeployer = await DeterministicDeployer.getAddress(new SimpleAccountFactory__factory(), 0, [this.entryPointAddress]) + const accountDeployer = await DeterministicDeployer.getAddress(new SimpleAccountFactory__factory(), 0, [ + this.entryPointAddress, + ]) + console.log('accountDeployer', accountDeployer) // const accountDeployer = await new SimpleAccountFactory__factory(this.provider.getSigner()).deploy().then(d=>d.address) - if (!await dep.isContractDeployed(accountDeployer)) { + if (!(await dep.isContractDeployed(accountDeployer))) { if (deploymentSigner == null) { console.log(`AccountDeployer not deployed at ${accountDeployer}. run with --deployFactory`) process.exit(1) @@ -66,26 +69,30 @@ class Runner { index: this.index, overheads: { // perUserOp: 100000 - } + }, }) return this } - parseExpectedGas (e: Error): Error { + parseExpectedGas(e: Error): Error { // parse a custom error generated by the BundlerHelper, which gives a hint of how much payment is missing const match = e.message?.match(/paid (\d+) expected (\d+)/) if (match != null) { const paid = Math.floor(parseInt(match[1]) / 1e9) const expected = Math.floor(parseInt(match[2]) / 1e9) - return new Error(`Error: Paid ${paid}, expected ${expected} . Paid ${Math.floor(paid / expected * 100)}%, missing ${expected - paid} `) + return new Error( + `Error: Paid ${paid}, expected ${expected} . Paid ${Math.floor((paid / expected) * 100)}%, missing ${ + expected - paid + } ` + ) } return e } - async runUserOp (target: string, data: string): Promise { + async runUserOp(target: string, data: string): Promise { const userOp = await this.accountApi.createSignedUserOp({ target, - data + data, }) try { const userOpHash = await this.bundlerProvider.sendUserOpToBundler(userOp) @@ -97,7 +104,7 @@ class Runner { } } -async function main (): Promise { +async function main(): Promise { const program = new Command() .version(erc4337RuntimeVersion) .option('--network ', 'network name or url', 'http://localhost:8545') @@ -125,7 +132,7 @@ async function main (): Promise { console.log('funding hardhat account', account) await signer.sendTransaction({ to: account, - value: parseEther('1').sub(bal) + value: parseEther('1').sub(bal), }) } @@ -158,16 +165,16 @@ async function main (): Promise { const accountOwner = new Wallet('0x'.padEnd(66, '7')) const index = opts.nonce ?? Date.now() - console.log('using account index=', index) - const client = await new Runner(provider, opts.bundlerUrl, accountOwner, opts.entryPoint, index).init(deployFactory ? signer : undefined) - + const client = await new Runner(provider, opts.bundlerUrl, accountOwner, opts.entryPoint, index).init( + deployFactory ? signer : undefined + ) const addr = await client.getAddress() - async function isDeployed (addr: string): Promise { - return await provider.getCode(addr).then(code => code !== '0x') + async function isDeployed(addr: string): Promise { + return await provider.getCode(addr).then((code) => code !== '0x') } - async function getBalance (addr: string): Promise { + async function getBalance(addr: string): Promise { return await provider.getBalance(addr) } @@ -178,10 +185,12 @@ async function main (): Promise { const requiredBalance = gasPrice.mul(4e6) if (bal.lt(requiredBalance.div(2))) { console.log('funding account to', requiredBalance.toString()) - await signer.sendTransaction({ - to: addr, - value: requiredBalance.sub(bal) - }).then(async tx => await tx.wait()) + await signer + .sendTransaction({ + to: addr, + value: requiredBalance.sub(bal), + }) + .then(async (tx) => await tx.wait()) } else { console.log('not funding account. balance is enough') } @@ -189,8 +198,8 @@ async function main (): Promise { const dest = addr const data = keccak256(Buffer.from('entryPoint()')).slice(0, 10) console.log('data=', data) - await client.runUserOp(dest, data) - console.log('after run1') + const res1 = await client.runUserOp(dest, data) + console.log('after run1: ', res1) // client.accountApi.overheads!.perUserOp = 30000 await client.runUserOp(dest, data) console.log('after run2') @@ -198,5 +207,8 @@ async function main (): Promise { } void main() - .catch(e => { console.log(e); process.exit(1) }) + .catch((e) => { + console.log(e) + process.exit(1) + }) .then(() => process.exit(0)) diff --git a/packages/bundler/test/Bundler.test.ts b/packages/bundler/test/Bundler.test.ts new file mode 100644 index 0000000..d073b9e --- /dev/null +++ b/packages/bundler/test/Bundler.test.ts @@ -0,0 +1,132 @@ +import { JsonRpcProvider } from '@ethersproject/providers' +import { Signer, Wallet, ethers } from 'ethers' +import { + IEntryPoint, + SimpleAccount, + SimpleAccount__factory, + SimpleAccountFactory, + SimpleAccountFactory__factory, + ISimpleAccountFactory, + IEntryPoint__factory, + UserOperation, + IEntryPointSimulations, + IEntryPointSimulations__factory, +} from '@account-abstraction/utils' +import axios from 'axios' +import { ValidationManager } from '@account-abstraction/validation-manager' +import { UserOpMethodHandler } from '../src/UserOpMethodHandler' +import { arrayify, hexlify, parseEther } from 'ethers/lib/utils' +import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' + +import { getUserOpHash } from '@account-abstraction/utils' + +import { packUserOp, resolveHexlify } from '../../utils' + +describe('BundlerSendOP', function () { + let provider: JsonRpcProvider + let owner: string + let wallet: Wallet + let signer: Signer + let entryPoint: IEntryPoint + let entryPointAddress: string + let smartAccount: SimpleAccount + let smartAccountAddress: string + let entryPointSimulationAddress: string + let entryPointSimulation: IEntryPointSimulations + let smartAccountFactoryAddress: string + let smartAccountFactory: SimpleAccountFactory + const key = 1 + + before(async function () { + provider = new ethers.providers.JsonRpcProvider('https://jsonrpc.euphoria.aura.network') + const mnemonic = process.env.MNEMONIC_TEST || '' + wallet = ethers.Wallet.fromMnemonic(mnemonic) + signer = wallet.connect(provider) + owner = '0x7875b83FEDF0d9FB12Fcb7D4351bE1FCE19a3ef7' + entryPointAddress = '0xfbC1a3AD32465bea6605d3bb7E6387caCa9337AC' + entryPoint = IEntryPoint__factory.connect(entryPointAddress, signer) + // smartAccountFactoryAddress = '0xa30F7A54b5f1102a01F6A590947BBBc6232F60C6' + // smartAccountFactory = SimpleAccountFactory__factory.connect(smartAccountFactoryAddress, signer) + // const res = await smartAccountFactory.createAccount(owner, '2432342342324234324234324234243223218') + // console.log('res', res) + smartAccountAddress = '0x4db902bd293768785c193c475f015da6135b2277' + smartAccount = SimpleAccount__factory.connect(smartAccountAddress, provider) + }) + + it('op should be sent success', async function () { + // prefund SA + if ((await provider.getBalance(smartAccountAddress)) < parseEther('0.2')) { + console.log('prefund account') + const res = await signer.sendTransaction({ to: smartAccountAddress, value: parseEther('0.5') }) + console.log('prefund SA res', res) + + // deposit to EntryPoint + await entryPoint.depositTo(smartAccountAddress, { value: parseEther('0.1') }) + } + + // sendUserOperation is async, even in auto-mining. need to wait for it. + const receivedAddr = '0xF4FC193579bCdA3172Fb7C49610e831b033D8d10' + const amount = '10000000000000000' // amount to send to RECEIVER_ADDR + const callData = smartAccount.interface.encodeFunctionData('execute', [receivedAddr, amount, '0x']) + if (callData === undefined) { + return + } + const sequenceNumber = await smartAccount.getNonce() + const nonce = await entryPoint.getNonce(smartAccountAddress, sequenceNumber) + + let op: UserOperation = { + sender: smartAccount.address, + nonce, + callData, + callGasLimit: 500000, + verificationGasLimit: 200000, + preVerificationGas: 50000, + maxFeePerGas: 1000000000, + maxPriorityFeePerGas: 1000000000, + signature: '', + } + + const signUserOp = async ( + op: UserOperation, + signer: Wallet, + entryPoint: string, + chainId: number + ): Promise => { + const message = getUserOpHash(op, entryPoint, chainId) + const signature = await signer.signMessage(arrayify(message)) + + return { + ...op, + signature, + } + } + const signedOp = await signUserOp(op, wallet, entryPointAddress, 6321) + const hexlifiedOp = await resolveHexlify(signedOp) + + const options = { + method: 'POST', + url: 'http://localhost:3000/rpc', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + data: { + jsonrpc: '2.0', + id: 1, + method: 'eth_sendUserOperation', + params: [hexlifiedOp, entryPointAddress], + }, + } + const res = await axios + .request(options) + .then(function (response) { + return response.data + }) + .catch(function (error) { + console.log('BundlerTest error', error) + }) + console.log('res', res) + let preDeposit = await entryPoint.balanceOf(smartAccountAddress) + console.log('preDeposit', preDeposit) + }) +}) diff --git a/packages/bundler/test/UserOpMethodHandler.test.ts b/packages/bundler/test/UserOpMethodHandler.test.ts index 58ef7ae..df2dcb4 100644 --- a/packages/bundler/test/UserOpMethodHandler.test.ts +++ b/packages/bundler/test/UserOpMethodHandler.test.ts @@ -8,11 +8,7 @@ import { toHex } from 'hardhat/internal/util/bigint' import { Signer, Wallet } from 'ethers' import { SimpleAccountAPI } from '@account-abstraction/sdk' import { postExecutionDump } from '@account-abstraction/utils/dist/src/postExecCheck' -import { - SampleRecipient, - TestRulesAccount, - TestRulesAccount__factory -} from '../src/types' +import { SampleRecipient, TestRulesAccount, TestRulesAccount__factory } from '../src/types' import { ValidationManager, supportsDebugTraceCall } from '@account-abstraction/validation-manager' import { deployEntryPoint, @@ -21,8 +17,9 @@ import { packUserOp, resolveHexlify, SimpleAccountFactory__factory, - UserOperation, UserOperationEventEvent, - waitFor + UserOperation, + UserOperationEventEvent, + waitFor, } from '@account-abstraction/utils' import { UserOperationReceipt } from '../src/RpcTypes' import { ExecutionManager } from '../src/modules/ExecutionManager' @@ -55,7 +52,9 @@ describe('UserOpMethodHandler', function () { signer = await createSigner() entryPoint = await deployEntryPoint(ethers.provider, ethers.provider.getSigner()) - accountDeployerAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [entryPoint.address]) + accountDeployerAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [ + entryPoint.address, + ]) const sampleRecipientFactory = await ethers.getContractFactory('SampleRecipient') sampleRecipient = await sampleRecipientFactory.deploy() @@ -68,30 +67,39 @@ 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) + const repMgr = new ReputationManager( + provider, + BundlerReputationParams, + parseEther(config.minStake), + config.minUnstakeDelay + ) mempoolMgr = new MempoolManager(repMgr) const validMgr = new ValidationManager(entryPoint, config.unsafe) const depositManager = new DepositManager(entryPoint, mempoolMgr) 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, depositManager) - methodHandler = new UserOpMethodHandler( - execManager, - provider, - signer, - config, - entryPoint + 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, depositManager) + methodHandler = new UserOpMethodHandler(execManager, provider, signer, config, entryPoint) }) describe('eth_supportedEntryPoints', function () { @@ -111,21 +119,25 @@ describe('UserOpMethodHandler', function () { 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' + data: '0xdeadface', }) - expect(await methodHandler.estimateUserOperationGas(await resolveHexlify(op), entryPoint.address).catch(e => e.message)).to.match(/AA21 didn't pay prefund/) + expect( + await methodHandler + .estimateUserOperationGas(await resolveHexlify(op), entryPoint.address) + .catch((e) => e.message) + ).to.match(/AA21 didn't pay prefund/) // should estimate with gasprice=0 const op1 = await smartAccountAPI.createSignedUserOp({ maxFeePerGas: 0, target, - data: '0xdeadface' + data: '0xdeadface', }) const ret = await methodHandler.estimateUserOperationGas(await resolveHexlify(op1), entryPoint.address) // verification gas should be high - it creates this wallet @@ -139,24 +151,27 @@ describe('UserOpMethodHandler', function () { it('estimateUserOperationGas should estimate using state overrides', async function () { const ver: string = await (provider as any).send('web3_clientVersion') if (ver.match('go1') == null) { - console.warn('\t==WARNING: test requires state override support on Geth (go-ethereum) node available after 1.12.1; ver=' + ver) + console.warn( + '\t==WARNING: test requires state override support on Geth (go-ethereum) node available after 1.12.1; ver=' + + ver + ) this.skip() } const op = await smartAccountAPI.createSignedUserOp({ target, - data: '0xdeadface' + data: '0xdeadface', }) - expect(await methodHandler.estimateUserOperationGas(await resolveHexlify(op), entryPoint.address).catch(e => e.message)).to.eql('FailedOp(0,"AA21 didn\'t pay prefund")') + expect( + await methodHandler + .estimateUserOperationGas(await resolveHexlify(op), entryPoint.address) + .catch((e) => e.message) + ).to.eql('FailedOp(0,"AA21 didn\'t pay prefund")') // should estimate same UserOperation with balance override set to 1 ether - const ret = await methodHandler.estimateUserOperationGas( - await resolveHexlify(op), - entryPoint.address, - { - [await op.sender]: { - balance: toHex(1e18) - } - } - ) + const ret = await methodHandler.estimateUserOperationGas(await resolveHexlify(op), entryPoint.address, { + [await op.sender]: { + balance: toHex(1e18), + }, + }) expect(ret.verificationGasLimit).to.be.closeTo(300000, 100000) expect(ret.callGasLimit).to.be.closeTo(25000, 10000) }) @@ -170,42 +185,53 @@ describe('UserOpMethodHandler', function () { let userOpHash: string before(async function () { DeterministicDeployer.init(ethers.provider) - accountDeployerAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [entryPoint.address]) + accountDeployerAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [ + entryPoint.address, + ]) const smartAccountAPI = new SimpleAccountAPI({ provider, entryPointAddress: entryPoint.address, owner: accountSigner, - factoryAddress: accountDeployerAddress + factoryAddress: accountDeployerAddress, }) accountAddress = await smartAccountAPI.getAccountAddress() await signer.sendTransaction({ to: accountAddress, - value: parseEther('1') + value: parseEther('1'), }) - userOperation = await resolveProperties(await smartAccountAPI.createSignedUserOp({ - data: sampleRecipient.interface.encodeFunctionData('something', [helloWorld]), - target: sampleRecipient.address - })) + 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 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 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' + 'UserOperationEvent', ]) - const [senderEvent] = await sampleRecipient.queryFilter(sampleRecipient.filters.Sender(), transactionReceipt.blockHash) + const [senderEvent] = await sampleRecipient.queryFilter( + sampleRecipient.filters.Sender(), + transactionReceipt.blockHash + ) const userOperationEvent = logs[3] assert.equal(userOperationEvent.args.success, true) @@ -231,11 +257,11 @@ 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 + target: sampleRecipient.address, }) try { @@ -252,28 +278,28 @@ describe('UserOpMethodHandler', function () { provider, entryPointAddress: entryPoint.address, accountAddress, - owner: accountSigner + owner: accountSigner, }) const op = await api.createSignedUserOp({ data: sampleRecipient.interface.encodeFunctionData('something', [helloWorld]), target: sampleRecipient.address, - gasLimit: 1e6 + 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 () => { + 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 + target: sampleRecipient.address, }) try { await methodHandler.sendUserOperation(await resolveHexlify(op), entryPoint.address) @@ -287,15 +313,15 @@ describe('UserOpMethodHandler', function () { describe('#_filterLogs', function () { // test events, good enough for _filterLogs - function userOpEv (hash: any): any { + function userOpEv(hash: any): any { return { - topics: ['userOpTopic', hash] + topics: ['userOpTopic', hash], } as any } - function ev (topic: any): UserOperationEventEvent { + function ev(topic: any): UserOperationEventEvent { return { - topics: [topic] + topics: [topic], } as any } @@ -336,13 +362,13 @@ describe('UserOpMethodHandler', function () { preVerificationGas: 50000, maxFeePerGas: 1e6, maxPriorityFeePerGas: 1e6, - signature: Buffer.from('emit-msg') + 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(packUserOp(op)) const beneficiary = signer.getAddress() - await entryPoint.handleOps([packUserOp(op)], beneficiary).then(async ret => await ret.wait()) + await entryPoint.handleOps([packUserOp(op)], beneficiary).then(async (ret) => await ret.wait()) const rcpt = await methodHandler.getUserOperationReceipt(userOpHash) if (rcpt == null) { throw new Error('getUserOperationReceipt returns null') @@ -364,23 +390,22 @@ describe('UserOpMethodHandler', function () { }) 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) } catch (e) { 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/packages/utils/src/DeterministicDeployer.ts b/packages/utils/src/DeterministicDeployer.ts index 95669dd..9f998d4 100644 --- a/packages/utils/src/DeterministicDeployer.ts +++ b/packages/utils/src/DeterministicDeployer.ts @@ -15,10 +15,10 @@ export class DeterministicDeployer { * @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 { + 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) } @@ -28,66 +28,70 @@ export class DeterministicDeployer { * @param salt optional salt. defaults to zero * @return 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 { + 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 deploymentTransaction = + '0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222' static deploymentSignerAddress = '0x3fab184622dc19b6109349b94811493bf2a45362' static deploymentGasPrice = 100e9 static deploymentGasLimit = 100000 - constructor ( - readonly provider: JsonRpcProvider, - readonly signer?: Signer) { - } + constructor(readonly provider: JsonRpcProvider, readonly signer?: Signer) {} - async isContractDeployed (address: string): Promise { - return await this.provider.getCode(address).then(code => code.length > 2) + async isContractDeployed(address: string): Promise { + return await this.provider.getCode(address).then((code) => code.length > 2) } - async isDeployerDeployed (): Promise { + async isDeployerDeployed(): Promise { return await this.isContractDeployed(DeterministicDeployer.proxyAddress) } - async deployFactory (): Promise { + async deployFactory(): Promise { if (await this.isContractDeployed(DeterministicDeployer.proxyAddress)) { return } const bal = await this.provider.getBalance(DeterministicDeployer.deploymentSignerAddress) - const neededBalance = BigNumber.from(DeterministicDeployer.deploymentGasLimit).mul(DeterministicDeployer.deploymentGasPrice) + const neededBalance = BigNumber.from(DeterministicDeployer.deploymentGasLimit).mul( + DeterministicDeployer.deploymentGasPrice + ) if (bal.lt(neededBalance)) { const signer = this.signer ?? this.provider.getSigner() await signer.sendTransaction({ to: DeterministicDeployer.deploymentSignerAddress, value: neededBalance, - gasLimit: DeterministicDeployer.deploymentGasLimit + gasLimit: DeterministicDeployer.deploymentGasLimit, + chainId: 6321, }) } await this.provider.send('eth_sendRawTransaction', [DeterministicDeployer.deploymentTransaction]) - if (!await this.isContractDeployed(DeterministicDeployer.proxyAddress)) { - throw new Error('raw TX didn\'t deploy deployer!') + 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 { + 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]), + chainId: 6321, } } - 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!) @@ -99,37 +103,42 @@ export class DeterministicDeployer { } } - 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 toChecksumAddress('0x' + keccak256(hexConcat([ - '0xff', - DeterministicDeployer.proxyAddress, - saltEncoded, - keccak256(ctrCode1) - ])).slice(-40)) + return toChecksumAddress( + '0x' + + keccak256(hexConcat(['0xff', DeterministicDeployer.proxyAddress, saltEncoded, keccak256(ctrCode1)])).slice(-40) + ) } - async deterministicDeploy (ctrCode: string | ContractFactory, salt: BigNumberish = 0, params: any[] = []): Promise { + async deterministicDeploy( + ctrCode: string | ContractFactory, + salt: BigNumberish = 0, + params: any[] = [] + ): Promise { const addr = DeterministicDeployer.getDeterministicDeployAddress(ctrCode, salt, params) - if (!await this.isContractDeployed(addr)) { + if (!(await this.isContractDeployed(addr))) { const signer = this.signer ?? this.provider.getSigner() - await signer.sendTransaction( - await this.getDeployTransaction(ctrCode, salt, params)) + await signer.sendTransaction(await this.getDeployTransaction(ctrCode, salt, params)) } return addr } private static _instance?: DeterministicDeployer - static init (provider: JsonRpcProvider, signer?: JsonRpcSigner): void { + 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') } diff --git a/yarn.lock b/yarn.lock index aff76ab..f61eacb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3265,6 +3265,15 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -4646,6 +4655,11 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + dotenv@~10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" @@ -5613,6 +5627,11 @@ follow-redirects@^1.12.1, follow-redirects@^1.14.0, follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"