From 18e93772298e78396bbbe0d40b51f40acd9130ec Mon Sep 17 00:00:00 2001 From: brianna Date: Wed, 8 Jul 2026 16:40:34 +0800 Subject: [PATCH 1/2] =?UTF-8?q?perf(evm):=20poll=20waitForCallsStatus=20at?= =?UTF-8?q?=20500ms=20=E2=80=94=20inclusion=20noticed=20~3s=20sooner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit waitForCallsStatus was called without a pollingInterval, so viem fell back to the client default (4s over http). A userOp included in ~2s (one Base block) sat unnoticed until the next 4s tick — measured live from the acp-cli trade loop: a same-chain swap spent ~7.4s between plan and confirmation report, most of it this dead wait. 500ms notices inclusion within half a second of reality; each tick is one light wallet_getCallsStatus read against the ACP RPC proxy. Override with ACP_CALLS_POLL_MS. Applies to both sendTransaction (ERC20-sponsored) and sendCalls (gasless) via the shared waitForTransactionHash helper. Co-Authored-By: Claude Opus 4.8 --- src/providers/evm/privyAlchemyEvmProviderAdapter.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/providers/evm/privyAlchemyEvmProviderAdapter.ts b/src/providers/evm/privyAlchemyEvmProviderAdapter.ts index f51095b..1856487 100644 --- a/src/providers/evm/privyAlchemyEvmProviderAdapter.ts +++ b/src/providers/evm/privyAlchemyEvmProviderAdapter.ts @@ -786,7 +786,18 @@ export class PrivyAlchemyEvmProviderAdapter implements IEvmProviderAdapter { client: SmartWalletClient, id: Hex ): Promise
{ - const status = await client.waitForCallsStatus({ id }); + // Poll inclusion fast. Without an explicit pollingInterval, viem falls + // back to the client's default (4s over http) — so a userOp that lands in + // ~2s sits unnoticed until the next 4s tick, which is the bulk of the + // user-perceived latency on an L2 send (2s block time). 500ms notices + // inclusion within half a second of reality; each tick is one light + // wallet_getCallsStatus read. Override with ACP_CALLS_POLL_MS. + const pollEnv = Number(process.env.ACP_CALLS_POLL_MS); + const status = await client.waitForCallsStatus({ + id, + pollingInterval: + Number.isFinite(pollEnv) && pollEnv > 0 ? pollEnv : 500, + }); if (!status.receipts?.[0]?.transactionHash) { throw new Error("Transaction failed"); } From cef3df86d44938970688b88ddca7cf2cb8b3af59 Mon Sep 17 00:00:00 2001 From: brianna Date: Thu, 9 Jul 2026 21:06:06 +0800 Subject: [PATCH 2/2] perf(evm): per-hop userOp timing behind ACP_TIMING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One log line per send splitting the pipeline into its serial hops — sendTransaction: prepare/sign/submit/confirm; sendCalls: submit (account-kit's internal prepare+sign+submit) and confirm (bundler inclusion + detection). The polling fix in this PR removes dead wait AFTER inclusion; these numbers show how much the bundler pipeline itself costs, i.e. whether a direct-7702 (no-bundler) fast path is worth building. Off by default; set ACP_TIMING=1 to enable. Co-Authored-By: Claude Fable 5 --- .../evm/privyAlchemyEvmProviderAdapter.ts | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/providers/evm/privyAlchemyEvmProviderAdapter.ts b/src/providers/evm/privyAlchemyEvmProviderAdapter.ts index 1856487..2a152e2 100644 --- a/src/providers/evm/privyAlchemyEvmProviderAdapter.ts +++ b/src/providers/evm/privyAlchemyEvmProviderAdapter.ts @@ -782,6 +782,27 @@ export class PrivyAlchemyEvmProviderAdapter implements IEvmProviderAdapter { }; } + // Per-hop latency telemetry for the userOp pipeline, gated behind ACP_TIMING + // (set to any value except 0/off). One line per send, so a live trace shows + // whether the time goes to the serial API hops (prepare/sign/submit) or to + // bundler inclusion — the split that decides whether a direct-7702 + // (no-bundler) fast path is worth building. + private static timingEnabled(): boolean { + const v = process.env.ACP_TIMING?.trim().toLowerCase(); + return !!v && v !== "0" && v !== "off" && v !== "false"; + } + private static logTiming( + op: string, + chainId: number, + marks: Record + ): void { + if (!PrivyAlchemyEvmProviderAdapter.timingEnabled()) return; + const parts = Object.entries(marks) + .map(([k, v]) => `${k}=${v}ms`) + .join(" "); + console.log(`[acp-timing] ${op} chainId=${chainId} ${parts}`); + } + private async waitForTransactionHash( client: SmartWalletClient, id: Hex @@ -810,17 +831,29 @@ export class PrivyAlchemyEvmProviderAdapter implements IEvmProviderAdapter { ): Promise
{ const smartWalletClientErc20 = this.getErc20Client(chainId); + const t0 = Date.now(); const prepared = await smartWalletClientErc20.prepareCalls({ calls: (Array.isArray(call) ? call : [call]).map((call) => this.toSmartWalletCall(call) ), }); + const t1 = Date.now(); const signed = await this.signPreparedViaPrivy(chainId, prepared); + const t2 = Date.now(); const { id } = await smartWalletClientErc20.sendPreparedCalls(signed); - - return this.waitForTransactionHash(smartWalletClientErc20, id); + const t3 = Date.now(); + + const hash = await this.waitForTransactionHash(smartWalletClientErc20, id); + PrivyAlchemyEvmProviderAdapter.logTiming("evm.sendTransaction", chainId, { + prepare: t1 - t0, + sign: t2 - t1, + submit: t3 - t2, + confirm: Date.now() - t3, + total: Date.now() - t0, + }); + return hash; } async sendCalls( @@ -828,6 +861,10 @@ export class PrivyAlchemyEvmProviderAdapter implements IEvmProviderAdapter { _calls: Call[] ): Promise
{ const smartWalletClient = this.getAcpClient(chainId); + // account-kit's sendCalls performs prepare + sign + submit internally, so + // "submit" here covers all three serial hops; "confirm" is bundler + // inclusion + detection. + const t0 = Date.now(); const { id } = await smartWalletClient.sendCalls({ calls: _calls.map((call) => this.toSmartWalletCall(call)), capabilities: { @@ -836,8 +873,15 @@ export class PrivyAlchemyEvmProviderAdapter implements IEvmProviderAdapter { }, }, }); + const t1 = Date.now(); - return this.waitForTransactionHash(smartWalletClient, id); + const hash = await this.waitForTransactionHash(smartWalletClient, id); + PrivyAlchemyEvmProviderAdapter.logTiming("evm.sendCalls", chainId, { + submit: t1 - t0, + confirm: Date.now() - t1, + total: Date.now() - t0, + }); + return hash; } async getTransactionReceipt(