From 0c82487b805efc85e0b697c6a0009cb45356b601 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Wed, 24 Jun 2026 16:56:42 -0700 Subject: [PATCH] fix(rain): 120s timeout on collateral withdraw submit (was 10s default) POST /rain/cards/withdraw/submit is synchronous: it broadcasts AND awaits on-chain confirmation (waitForUserOperationReceipt + confirmIntentByTxHash) and, for a request/charge, settles the charge in the same call. The FE's submitWithdrawal sent it with no timeoutMs, so it used fetchWithSentry's default 10s budget. When confirmation exceeds 10s the FE aborts while the tx still lands and the charge settles: the payer sees "there was an issue with your request" on a payment that SUCCEEDED, retries, and double-sends (observed in prod: one payer -> same recipient x3). #2245 (73f73bc53) routed request payments through this submit path for the first time, which is why request-pays only started timing out now. Match the 120s budget the verified-withdrawal path already uses for the same synchronous-confirm reason. Fixes PEANUT-UI-QP5 (rain/cards/withdraw/submit timed out after 10000ms). --- src/services/rain.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/rain.ts b/src/services/rain.ts index 73463e25b..b494ab2a6 100644 --- a/src/services/rain.ts +++ b/src/services/rain.ts @@ -392,12 +392,23 @@ export const rainApi = { * Submit a prepared withdrawal with the user's admin signature. Backend * verifies via ERC-1271 against the user's kernel and broadcasts the * coordinator call through the shared admin relayer. + * + * `/submit` is SYNCHRONOUS: it broadcasts AND awaits on-chain confirmation + * (`waitForUserOperationReceipt` + `confirmIntentByTxHash`) before + * responding, and for a request/charge it settles the charge in the same + * call. That round-trip routinely exceeds the default 10s fetch budget, so + * pass 120s — the same budget the verified-withdrawal path already uses for + * this exact reason (see line ~525). With the 10s default the FE aborts + * while the tx still lands + the charge settles: the user sees an error on a + * payment that actually succeeded, retries, and double-sends. (#2245 routed + * request payments through this path for the first time → the regression.) */ submitWithdrawal: async (input: SubmitRainWithdrawalInput): Promise => { return rainRequest({ method: 'POST', path: '/rain/cards/withdraw/submit', body: input, + timeoutMs: 120_000, }) },