From 03b55746b9fb4bbfc8406eb5e04e78b6dc8affea Mon Sep 17 00:00:00 2001 From: mous <97399882+mouseless0x@users.noreply.github.com> Date: Sat, 14 Dec 2024 01:57:22 +0700 Subject: [PATCH] Fix/replacement logic flow (#378) * fix replacement logic flow * add retry logic to resubmissions * fix build --------- Co-authored-by: mouseless <97399882+mouseless-eth@users.noreply.github.com> --- src/executor/executor.ts | 198 ++++++++++++++++---------------- src/executor/executorManager.ts | 24 ++++ src/types/mempool.ts | 3 +- 3 files changed, 121 insertions(+), 104 deletions(-) diff --git a/src/executor/executor.ts b/src/executor/executor.ts index b4b255e7..c8ce5a9d 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -18,7 +18,6 @@ import { type PackedUserOperation, type TransactionInfo, type UserOperation, - type UserOperationV06, type UserOperationV07, type UserOperationWithHash, deriveUserOperation, @@ -66,6 +65,18 @@ export interface GasEstimateResult { callGasLimit: bigint } +export type HandleOpsTxParam = + | { + type: "default" + ops: PackedUserOperation[] + isUserOpVersion06: boolean + entryPoint: Address + } + | { + type: "compressed" + compressedOps: CompressedUserOperation[] + } + export type ReplaceTransactionResult = | { status: "replaced" @@ -333,6 +344,7 @@ export class Executor { newRequest.gas = maxBigInt(newRequest.gas, gasLimit) // update calldata to include only ops that pass simulation + let txParam: HandleOpsTxParam if (transactionInfo.transactionType === "default") { const isUserOpVersion06 = opsWithHashes.reduce( (acc, op) => { @@ -351,39 +363,29 @@ export class Executor { ) ) - newRequest.data = isUserOpVersion06 - ? encodeFunctionData({ - abi: EntryPointV06Abi, - functionName: "handleOps", - args: [ - opsToBundle.map( - (opInfo) => - opInfo.mempoolUserOperation as UserOperationV06 - ), - transactionInfo.executor.address - ] - }) - : encodeFunctionData({ - abi: EntryPointV07Abi, - functionName: "handleOps", - args: [ - opsToBundle.map((opInfo) => - toPackedUserOperation( - opInfo.mempoolUserOperation as UserOperationV07 - ) - ), - transactionInfo.executor.address - ] - }) - } else if (transactionInfo.transactionType === "compressed") { + const userOps = opsToBundle.map((op) => + isUserOpVersion06 + ? op.mempoolUserOperation + : toPackedUserOperation( + op.mempoolUserOperation as UserOperationV07 + ) + ) as PackedUserOperation[] + + txParam = { + type: "default", + isUserOpVersion06, + ops: userOps, + entryPoint: transactionInfo.entryPoint + } + } else { const compressedOps = opsToBundle.map( - (opInfo) => - opInfo.mempoolUserOperation as CompressedUserOperation - ) - newRequest.data = createCompressedCalldata( - compressedOps, - this.getCompressionHandler().perOpInflatorId - ) + ({ mempoolUserOperation }) => mempoolUserOperation + ) as CompressedUserOperation[] + + txParam = { + type: "compressed", + compressedOps: compressedOps + } } try { @@ -402,25 +404,30 @@ export class Executor { "replacing transaction" ) - const txHash = await this.config.walletClient.sendTransaction( - this.config.legacyTransactions + const txHash = await this.sendHandleOpsTransaction({ + txParam, + opts: this.config.legacyTransactions ? { - ...newRequest, + account: newRequest.account, gasPrice: newRequest.maxFeePerGas, - maxFeePerGas: undefined, - maxPriorityFeePerGas: undefined, - type: "legacy", - accessList: undefined + gas: newRequest.gas, + nonce: newRequest.nonce } - : newRequest - ) + : { + account: newRequest.account, + maxFeePerGas: newRequest.maxFeePerGas, + maxPriorityFeePerGas: newRequest.maxPriorityFeePerGas, + gas: newRequest.gas, + nonce: newRequest.nonce + } + }) - opsToBundle.map((opToBundle) => { - const op = deriveUserOperation(opToBundle.mempoolUserOperation) + opsToBundle.map(({ entryPoint, mempoolUserOperation }) => { + const op = deriveUserOperation(mempoolUserOperation) const chainId = this.config.publicClient.chain?.id const opHash = getUserOperationHash( op, - opToBundle.entryPoint, + entryPoint, chainId as number ) @@ -489,6 +496,7 @@ export class Executor { childLogger.warn({ error: e }, "error replacing transaction") this.markWalletProcessed(transactionInfo.executor) + return { status: "failed" } } } @@ -525,10 +533,11 @@ export class Executor { await Promise.all(promises) } - async sendHandleOpsTransaction( - userOps: PackedUserOperation[], - isUserOpVersion06: boolean, - entryPoint: Address, + async sendHandleOpsTransaction({ + txParam, + opts + }: { + txParam: HandleOpsTxParam opts: | { gasPrice: bigint @@ -546,17 +555,32 @@ export class Executor { gas: bigint nonce: number } - ) { + }) { + let data: Hex + let to: Address + + if (txParam.type === "default") { + const { isUserOpVersion06, ops, entryPoint } = txParam + data = encodeFunctionData({ + abi: isUserOpVersion06 ? EntryPointV06Abi : EntryPointV07Abi, + functionName: "handleOps", + args: [ops, opts.account.address] + }) + to = entryPoint + } else { + const { compressedOps } = txParam + const compressionHandler = this.getCompressionHandler() + data = createCompressedCalldata( + compressedOps, + compressionHandler.perOpInflatorId + ) + to = compressionHandler.bundleBulkerAddress + } + const request = await this.config.walletClient.prepareTransactionRequest({ - to: entryPoint, - data: encodeFunctionData({ - abi: isUserOpVersion06 - ? EntryPointV06Abi - : EntryPointV07Abi, - functionName: "handleOps", - args: [userOps, opts.account.address] - }), + to, + data, ...opts }) @@ -881,12 +905,15 @@ export class Executor { ) ) as PackedUserOperation[] - transactionHash = await this.sendHandleOpsTransaction( - userOps, - isUserOpVersion06, - entryPoint, + transactionHash = await this.sendHandleOpsTransaction({ + txParam: { + type: "default", + ops: userOps, + isUserOpVersion06, + entryPoint + }, opts - ) + }) opsWithHashToBundle.map(({ userOperationHash }) => { this.eventManager.emitSubmitted( @@ -953,30 +980,6 @@ export class Executor { transactionRequest: { account: wallet, to: ep.address, - data: isUserOpVersion06 - ? encodeFunctionData({ - abi: ep.abi, - functionName: "handleOps", - args: [ - opsWithHashToBundle.map( - (owh) => - owh.mempoolUserOperation as UserOperationV06 - ), - wallet.address - ] - }) - : encodeFunctionData({ - abi: ep.abi, - functionName: "handleOps", - args: [ - opsWithHashToBundle.map((owh) => - toPackedUserOperation( - owh.mempoolUserOperation as UserOperationV07 - ) - ), - wallet.address - ] - }), gas: gasLimit, chain: this.config.walletClient.chain, maxFeePerGas: gasPriceParameters.maxFeePerGas, @@ -1156,17 +1159,12 @@ export class Executor { } ) - // need to use sendTransaction to target BundleBulker's fallback - transactionHash = await this.config.walletClient.sendTransaction({ - account: wallet, - to: compressionHandler.bundleBulkerAddress, - data: createCompressedCalldata( - compressedOpsToBundle, - compressionHandler.perOpInflatorId - ), - gas: gasLimit, - nonce: nonce, - ...gasOptions + transactionHash = await this.sendHandleOpsTransaction({ + txParam: { + type: "compressed", + compressedOps: compressedOpsToBundle + }, + opts: { ...gasOptions, gas: gasLimit, account: wallet, nonce } }) opsToBundle.map(({ userOperationHash }) => { @@ -1215,10 +1213,6 @@ export class Executor { previousTransactionHashes: [], transactionRequest: { to: compressionHandler.bundleBulkerAddress, - data: createCompressedCalldata( - compressedOps, - compressionHandler.perOpInflatorId - ), gas: gasLimit, account: wallet, chain: this.config.walletClient.chain, diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index a41f47f7..7291299f 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -823,6 +823,7 @@ export class ExecutorManager { reason: string ): Promise { let replaceResult: ReplaceTransactionResult | undefined = undefined + try { replaceResult = await this.executor.replaceTransaction(txInfo) } finally { @@ -830,8 +831,29 @@ export class ExecutorManager { .labels({ reason, status: replaceResult?.status || "failed" }) .inc() } + if (replaceResult.status === "failed") { txInfo.userOperationInfos.map((opInfo) => { + const userOperation = deriveUserOperation( + opInfo.mempoolUserOperation + ) + + this.eventManager.emitDropped( + opInfo.userOperationHash, + "Failed to replace transaction" + ) + + this.logger.warn( + { + userOperation: JSON.stringify(userOperation, (_k, v) => + typeof v === "bigint" ? v.toString() : v + ), + userOpHash: opInfo.userOperationHash, + reason + }, + "user operation rejected" + ) + this.mempool.removeSubmitted(opInfo.userOperationHash) }) @@ -842,6 +864,7 @@ export class ExecutorManager { return } + if (replaceResult.status === "potentially_already_included") { this.logger.info( { oldTxHash: txInfo.transactionHash, reason }, @@ -872,6 +895,7 @@ export class ExecutorManager { .map((ni) => ni.userOperationHash) .includes(info.userOperationHash) ) + const matchingOps = txInfo.userOperationInfos.filter((info) => newTxInfo.userOperationInfos .map((ni) => ni.userOperationHash) diff --git a/src/types/mempool.ts b/src/types/mempool.ts index 3b77df47..6a436af7 100644 --- a/src/types/mempool.ts +++ b/src/types/mempool.ts @@ -1,4 +1,4 @@ -import type { Address, Chain, Hex } from "viem" +import type { Address, Chain } from "viem" import type { Account } from "viem/accounts" import type { CompressedUserOperation, HexData32, UserOperation } from "." @@ -33,7 +33,6 @@ export type TransactionInfo = { transactionRequest: { account: Account to: Address - data: Hex gas: bigint chain: Chain maxFeePerGas: bigint