diff --git a/src/providers/evm/privyAlchemyEvmProviderAdapter.ts b/src/providers/evm/privyAlchemyEvmProviderAdapter.ts index f51095b..2a152e2 100644 --- a/src/providers/evm/privyAlchemyEvmProviderAdapter.ts +++ b/src/providers/evm/privyAlchemyEvmProviderAdapter.ts @@ -782,11 +782,43 @@ 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 ): 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"); } @@ -799,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( @@ -817,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: { @@ -825,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(