Skip to content

[sdk, simplex, docs]: retry bundler gas estimation on transient errors - #1406

Merged
seunlanlege merged 2 commits into
mainfrom
simplex/retry-bundler-gas-estimation
Oct 15, 2026
Merged

seunlanlege merged 2 commits into
mainfrom
simplex/retry-bundler-gas-estimation

Conversation

@seunlanlege

@seunlanlege seunlanlege commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Why

On 2026-10-15 at 13:02:34 UTC the Simplex filler skipped a $298 BSC to Base order. One eth_estimateUserOperationGas call to our rundler bundler failed because rundler's upstream RPC (dRPC) dropped a connection, and rundler answered JSON-RPC -32603 internal error: rpc provider error. The filler requires the bundler's own estimate (requireBundlerEstimate), so it logged Error estimating gas, not bidding on this order and never asked again. Nobody filled the order and the user was refunded. A second request a moment later would almost certainly have succeeded.

What changes

  • GasEstimator.estimateFillOrder and GasEstimator.estimateBidPreVerificationGas (@hyperbridge/sdk) now send eth_estimateUserOperationGas through retryTransientBundlerErrors (new sdk/packages/sdk/src/protocols/intents/bundlerRetry.ts). It makes at most 3 attempts, waiting 250 ms and then 750 ms, so a failing estimate waits at most 1 s longer before the order is skipped as before.
  • CryptoUtils.sendBundler now throws a BundlerRpcError carrying the JSON-RPC code/data, the HTTP status and a transport flag. The message is still Bundler error: <message> for JSON-RPC errors. It also stops returning undefined on an HTTP error with no JSON-RPC error body: that case now throws Bundler error: HTTP <status>.
  • Retried (isTransientBundlerError): a request or body that failed in transit (fetch failed, connection reset/refused, timeouts), JSON-RPC -32603, and HTTP 429/5xx.
  • Not retried, fails on the first attempt as now: any message naming an AA.. EntryPoint revert, the ERC-4337 range -32500..-32599 (-32500, -32501, -32502, -32521, ...), -32700 and -32600..-32602 (invalid params, including rundler's Unsupported entry point), whatever the HTTP status. Also not retried: any other JSON-RPC code on a non-429/5xx response, and any error that did not come from sendBundler (e.g. No EntryPoint configured).
  • Sends are not retried. eth_sendUserOperation goes through sendBundler only in Bid.execute, which sends once. Simplex's UserOpSender uses its own client, and its contract is that a rejected send was never submitted, so the caller may fall back to a native transaction. A send whose response was lost may already be in the mempool, so neither path treats a resend as idempotent, and this PR leaves both alone.
  • Logging: EstimateFillOrderParams and BidPreVerificationGasParams take an optional onBundlerRetry(event: BundlerRetryEvent), with attempt, maxAttempts, delayMs and error. Without it the SDK writes a console.warn. Simplex's ContractInteractionService passes a hook that logs WARN contract-service Bundler gas estimate failed transiently, retrying { orderId, attempt, maxAttempts, delayMs, err }, so blips show up in the filler log and on the dashboard Logs page.
  • Docs: sdk/packages/simplex/docs/ai/changelog/2026-10-15-retry-transient-bundler-gas-estimates.md, plus the onBundlerRetry row and the retry behaviour under estimateFillOrder in docs/content/developers/sdk/api/intent-gateway.mdx. No version bumps: releases here are tag-driven. The bundled simplex binary and Docker image pick this up from the workspace SDK, and library embedders need the next @hyperbridge/sdk release.

Validation

  • pnpm --filter @hyperbridge/sdk typecheck: passes.
  • SDK vitest --watch=false src/tests/bundlerRetry.test.ts src/tests/fillEstimateBundlerFailure.test.ts src/tests/bidPreVerificationGas.test.ts src/tests/fillEstimateGasPrice.test.ts: 34/34 pass. These include, over a stubbed fetch through the real sendBundler: a transient error then success succeeds; a transient error on every attempt fails after 3 attempts with the bundler's error; an AA23 validation revert is not retried. The two retry cases fail when the retry is removed.
  • SDK test:concurrent: 336 passed, 6 skipped. One file fails, stateCalls.test.ts, which needs .env.local RPC URLs that this machine does not have. It is unrelated to this change.
  • Simplex: lint exits 0 (87 warnings, same as main); test:unit 261/261, test:data 146/146, test:filler 59 passed / 5 skipped, vitest src/tests/services 189 passed / 3 skipped (including the new retry-logging test in bid-gas-estimate.test.ts); pnpm --filter @hyperbridge/simplex build succeeds (tsup + DTS + UI). The bundled dist/bin/simplex.js contains the retry.
  • Biome on the changed files reports no new diagnostics. The SDK-wide biome lint . already fails on main and is not part of CI.

A single failed eth_estimateUserOperationGas made the Simplex filler skip
an order, since it requires the bundler's own estimate. On 2026-10-15 a
BSC->Base order was skipped and refunded because rundler's upstream RPC
dropped one connection and rundler answered -32603 "internal error: rpc
provider error".

GasEstimator.estimateFillOrder and estimateBidPreVerificationGas now send
the estimate through retryTransientBundlerErrors: at most 3 attempts,
250 ms then 750 ms apart. CryptoUtils.sendBundler throws a BundlerRpcError
carrying the JSON-RPC code, HTTP status and a transport flag (message
unchanged for JSON-RPC errors). Network failures, JSON-RPC -32603 and
HTTP 429/5xx are retried; AA.. reverts, the ERC-4337 -325xx range and
invalid params (-32602) fail on the first attempt as before.
eth_sendUserOperation is not retried.

Both params types take onBundlerRetry (default console.warn); Simplex's
ContractInteractionService logs each retry as a warning with the order
id, attempt and error.
@seunlanlege
seunlanlege force-pushed the simplex/retry-bundler-gas-estimation branch from a8b38ff to 3a4fe3e Compare October 15, 2026 15:29
… API

Address review feedback on the transient bundler gas-estimate retry:

- Bound each sendBundler request with AbortSignal.timeout(BUNDLER_REQUEST_TIMEOUT_MS)
  so a stalled connection cannot hold up a fill bid past the retries' budget.
- Also retry HTTP 408 as transient.
- Move onBundlerRetry out of BidPreVerificationGasParams into a second argument of
  estimateBidPreVerificationGas, keeping the bid-fields type pure.
- Re-export BundlerRpcError, isTransientBundlerError, retryTransientBundlerErrors and
  the retry/timeout constants from @hyperbridge/sdk.
- Document the sendBundler throw change (HTTP error with no JSON-RPC body now throws)
  and the timeout.
@seunlanlege
seunlanlege merged commit 815dd98 into main Oct 15, 2026
4 of 6 checks passed
@seunlanlege
seunlanlege deleted the simplex/retry-bundler-gas-estimation branch October 15, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant