diff --git a/src/executor/executor.ts b/src/executor/executor.ts index 17869355..976f9792 100644 --- a/src/executor/executor.ts +++ b/src/executor/executor.ts @@ -166,7 +166,7 @@ export class Executor { ): Promise<ReplaceTransactionResult> { const newRequest = { ...transactionInfo.transactionRequest } - let gasPriceParameters + let gasPriceParameters: GasPriceParameters try { gasPriceParameters = await this.gasPriceManager.tryGetNetworkGasPrice() diff --git a/src/executor/executorManager.ts b/src/executor/executorManager.ts index 62656a50..0cf9e05b 100644 --- a/src/executor/executorManager.ts +++ b/src/executor/executorManager.ts @@ -48,6 +48,11 @@ function getTransactionsFromUserOperationEntries( ) } +const MIN_INTERVAL = 100 // 0.1 seconds (100ms) +const MAX_INTERVAL = 1000 // Capped at 1 second (1000ms) +const SCALE_FACTOR = 10 // Interval increases by 5ms per task per minute +const RPM_WINDOW = 60000 // 1 minute window in ms + export class ExecutorManager { private config: AltoConfig private executor: Executor @@ -58,9 +63,10 @@ export class ExecutorManager { private reputationManager: InterfaceReputationManager private unWatch: WatchBlocksReturnType | undefined private currentlyHandlingBlock = false - private timer?: NodeJS.Timer private gasPriceManager: GasPriceManager private eventManager: EventManager + private opsCount: number[] = [] + private bundlingMode: BundlingMode constructor({ config, @@ -96,24 +102,76 @@ export class ExecutorManager { this.gasPriceManager = gasPriceManager this.eventManager = eventManager - if (this.config.bundleMode === "auto") { - this.timer = setInterval(async () => { - await this.bundle() - }, this.config.maxBundleWait) as NodeJS.Timer + this.bundlingMode = this.config.bundleMode + + if (this.bundlingMode === "auto") { + this.autoScalingBundling() } } - setBundlingMode(bundleMode: BundlingMode): void { - if (bundleMode === "auto" && !this.timer) { - this.timer = setInterval(async () => { - await this.bundle() - }, this.config.maxBundleWait) as NodeJS.Timer - } else if (bundleMode === "manual" && this.timer) { - clearInterval(this.timer) - this.timer = undefined + async setBundlingMode(bundleMode: BundlingMode): Promise<void> { + this.bundlingMode = bundleMode + + if (bundleMode === "manual") { + await new Promise((resolve) => + setTimeout(resolve, 2 * MAX_INTERVAL) + ) + } + + if (bundleMode === "auto") { + this.autoScalingBundling() } } + async autoScalingBundling() { + const now = Date.now() + this.opsCount = this.opsCount.filter( + (timestamp) => now - timestamp < RPM_WINDOW + ) + + const opsToBundle = await this.getOpsToBundle() + + if (opsToBundle.length > 0) { + const opsCount: number = opsToBundle.length + const timestamp: number = Date.now() + this.opsCount.push(...Array(opsCount).fill(timestamp)) // Add timestamps for each task + + await this.bundle(opsToBundle) + } + + const rpm: number = this.opsCount.length + // Calculate next interval with linear scaling + const nextInterval: number = Math.min( + MIN_INTERVAL + rpm * SCALE_FACTOR, // Linear scaling + MAX_INTERVAL // Cap at 1000ms + ) + if (this.bundlingMode === "auto") { + setTimeout(this.autoScalingBundling.bind(this), nextInterval) + } + } + + async getOpsToBundle() { + const opsToBundle: UserOperationInfo[][] = [] + + while (true) { + const ops = await this.mempool.process( + this.config.maxGasPerBundle, + 1 + ) + if (ops?.length > 0) { + opsToBundle.push(ops) + } else { + break + } + } + + if (opsToBundle.length === 0) { + return [] + } + + return opsToBundle + } + async bundleNow(): Promise<Hash[]> { const ops = await this.mempool.process(this.config.maxGasPerBundle, 1) if (ops.length === 0) { @@ -285,25 +343,7 @@ export class ExecutorManager { return txHash } - async bundle() { - const opsToBundle: UserOperationInfo[][] = [] - - while (true) { - const ops = await this.mempool.process( - this.config.maxGasPerBundle, - 1 - ) - if (ops?.length > 0) { - opsToBundle.push(ops) - } else { - break - } - } - - if (opsToBundle.length === 0) { - return - } - + async bundle(opsToBundle: UserOperationInfo[][] = []) { await Promise.all( opsToBundle.map(async (ops) => { const opEntryPointMap = new Map< diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index de26fa3c..29de477e 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -82,7 +82,10 @@ import { import { base, baseSepolia, optimism } from "viem/chains" import type { NonceQueuer } from "./nonceQueuer" import type { AltoConfig } from "../createConfig" -import { SignedAuthorization, SignedAuthorizationList } from "viem/experimental" +import type { + SignedAuthorization, + SignedAuthorizationList +} from "viem/experimental" export interface IRpcEndpoint { handleMethod( @@ -248,7 +251,7 @@ export class RpcHandler implements IRpcEndpoint { case "debug_bundler_setBundlingMode": return { method, - result: this.debug_bundler_setBundlingMode( + result: await this.debug_bundler_setBundlingMode( ...request.params ) } @@ -588,12 +591,12 @@ export class RpcHandler implements IRpcEndpoint { return transactions[0] } - debug_bundler_setBundlingMode( + async debug_bundler_setBundlingMode( bundlingMode: BundlingMode - ): BundlerSetBundlingModeResponseResult { + ): Promise<BundlerSetBundlingModeResponseResult> { this.ensureDebugEndpointsAreEnabled("debug_bundler_setBundlingMode") - this.executorManager.setBundlingMode(bundlingMode) + await this.executorManager.setBundlingMode(bundlingMode) return "ok" } diff --git a/test/e2e/alto-config.json b/test/e2e/alto-config.json index 7366710a..3e6c344a 100644 --- a/test/e2e/alto-config.json +++ b/test/e2e/alto-config.json @@ -20,5 +20,6 @@ "mempool-max-queued-ops": 10, "enforce-unique-senders-per-bundle": false, "code-override-support": true, - "enable-instant-bundling-endpoint": true + "enable-instant-bundling-endpoint": true, + "bundling-mode": "manual" } diff --git a/test/e2e/setup.ts b/test/e2e/setup.ts index 2199e496..5c1c9146 100644 --- a/test/e2e/setup.ts +++ b/test/e2e/setup.ts @@ -116,7 +116,8 @@ export default async function setup({ provide }) { const anvilInstance = anvil({ chainId: foundry.id, - port: 8485 + port: 8485, + codeSizeLimit: 1000_000 }) await anvilInstance.start() const anvilRpc = `http://${anvilInstance.host}:${anvilInstance.port}` diff --git a/test/e2e/tests/eth_sendUserOperation.test.ts b/test/e2e/tests/eth_sendUserOperation.test.ts index e3aa4bf3..d5925389 100644 --- a/test/e2e/tests/eth_sendUserOperation.test.ts +++ b/test/e2e/tests/eth_sendUserOperation.test.ts @@ -180,16 +180,16 @@ describe.each([ ] }) - await expect(async () => { - await firstClient.getUserOperationReceipt({ + await expect(() => + firstClient.getUserOperationReceipt({ hash: firstHash }) - }).rejects.toThrow(UserOperationReceiptNotFoundError) - await expect(async () => { - await secondClient.getUserOperationReceipt({ + ).rejects.toThrow(UserOperationReceiptNotFoundError) + await expect(() => + secondClient.getUserOperationReceipt({ hash: secondHash }) - }).rejects.toThrow(UserOperationReceiptNotFoundError) + ).rejects.toThrow(UserOperationReceiptNotFoundError) await sendBundleNow({ altoRpc }) @@ -359,12 +359,7 @@ describe.each([ const nonceValueDiffs = [0n, 1n, 2n] // Send 3 sequential user ops - const sendUserOperation = async (nonceValueDiff: bigint) => { - const nonce = (await entryPointContract.read.getNonce([ - client.account.address, - nonceKey - ])) as bigint - + const sendUserOperation = (nonce: bigint) => { return client.sendUserOperation({ calls: [ { @@ -373,13 +368,17 @@ describe.each([ data: "0x" } ], - nonce: nonce + nonceValueDiff + nonce: nonce }) } const opHashes: Hex[] = [] + const nonce = (await entryPointContract.read.getNonce([ + client.account.address, + nonceKey + ])) as bigint for (const nonceValueDiff of nonceValueDiffs) { - opHashes.push(await sendUserOperation(nonceValueDiff)) + opHashes.push(await sendUserOperation(nonce + nonceValueDiff)) } await sendBundleNow({ altoRpc })