From cefa158d5c2ec2613782a39749fc4dc62303f4aa Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 3 Apr 2026 09:06:17 +0100 Subject: [PATCH 1/2] [#788] Fix stale nonce in Zap two-step flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 500ms delay after approval waitForTransactionReceipt in all three approve→write paths (Zap ERC-20 buy, PLOT buy, sell). Gives the wallet provider time to update its internal nonce before the second writeContractAsync call, preventing "nonce too low" simulation failures. Fixes #788 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/TradingWidget.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/TradingWidget.tsx b/src/components/TradingWidget.tsx index efb51387..6d894fdc 100644 --- a/src/components/TradingWidget.tsx +++ b/src/components/TradingWidget.tsx @@ -312,6 +312,8 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { args: [ZAP_PLOTLINK, zapQuote.fromTokenAmount], }); await publicClient.waitForTransactionReceipt({ hash: approveHash }); + // Allow wallet provider to update its internal nonce after approval + await new Promise((r) => setTimeout(r, 500)); } } @@ -342,6 +344,8 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { args: [MCV2_BOND, maxCost], }); await publicClient.waitForTransactionReceipt({ hash: approveHash }); + // Allow wallet provider to update its internal nonce after approval + await new Promise((r) => setTimeout(r, 500)); } setTxState("confirming"); @@ -376,6 +380,8 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { args: [MCV2_BOND, parsedAmount], }); await publicClient.waitForTransactionReceipt({ hash: approveHash }); + // Allow wallet provider to update its internal nonce after approval + await new Promise((r) => setTimeout(r, 500)); } setTxState("confirming"); From 9acc99112a81446458e38a30c7902f8f1e5a3aff Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 3 Apr 2026 09:07:52 +0100 Subject: [PATCH 2/2] [#788] Replace blind sleep with retry-on-nonce-error Instead of a hardcoded 500ms delay, wrap the post-approval writeContractAsync calls in retryOnNonceError(). On first attempt, if the wallet provider's nonce cache is stale, the nonce error is caught and retried once after 500ms. No delay on fast providers; reliable recovery on slow ones. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/TradingWidget.tsx | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/TradingWidget.tsx b/src/components/TradingWidget.tsx index 6d894fdc..232df269 100644 --- a/src/components/TradingWidget.tsx +++ b/src/components/TradingWidget.tsx @@ -28,6 +28,20 @@ function applySlippage(amount: bigint, isBuy: boolean): bigint { const isZapAvailable = ZAP_PLOTLINK !== "0x0000000000000000000000000000000000000000"; +/** Retry a writeContractAsync call once if it fails with a nonce error. */ +async function retryOnNonceError(fn: () => Promise): Promise { + try { + return await fn(); + } catch (err) { + const msg = err instanceof Error ? err.message : ""; + if (msg.includes("nonce") && msg.includes("low")) { + await new Promise((r) => setTimeout(r, 500)); + return await fn(); + } + throw err; + } +} + function getTokenDecimals(payToken: PayToken): number { if (payToken === "USDC") return 6; return 18; @@ -312,14 +326,12 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { args: [ZAP_PLOTLINK, zapQuote.fromTokenAmount], }); await publicClient.waitForTransactionReceipt({ hash: approveHash }); - // Allow wallet provider to update its internal nonce after approval - await new Promise((r) => setTimeout(r, 500)); } } setTxState("confirming"); const tx = buildZapMintTx(fromToken, tokenAddress, parsedAmount, "exact-output", zapQuote); - const hash = await writeContractAsync(tx); + const hash = await retryOnNonceError(() => writeContractAsync(tx)); setTxHash(hash); tradeHash = hash; setTxState("pending"); @@ -344,18 +356,16 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { args: [MCV2_BOND, maxCost], }); await publicClient.waitForTransactionReceipt({ hash: approveHash }); - // Allow wallet provider to update its internal nonce after approval - await new Promise((r) => setTimeout(r, 500)); } setTxState("confirming"); - const hash = await writeContractAsync({ + const hash = await retryOnNonceError(() => writeContractAsync({ address: MCV2_BOND, abi: mcv2BondAbi, functionName: "mint", args: [tokenAddress, parsedAmount, maxCost, address], gas: BigInt(2_000_000), - }); + })); setTxHash(hash); tradeHash = hash; setTxState("pending"); @@ -380,18 +390,16 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { args: [MCV2_BOND, parsedAmount], }); await publicClient.waitForTransactionReceipt({ hash: approveHash }); - // Allow wallet provider to update its internal nonce after approval - await new Promise((r) => setTimeout(r, 500)); } setTxState("confirming"); - const hash = await writeContractAsync({ + const hash = await retryOnNonceError(() => writeContractAsync({ address: MCV2_BOND, abi: mcv2BondAbi, functionName: "burn", args: [tokenAddress, parsedAmount, minRefund, address], gas: BigInt(2_000_000), - }); + })); setTxHash(hash); tradeHash = hash; setTxState("pending");