improve(tx-utils): preserve simulation-failure reason through willSucceed fallbacks#3466
Conversation
…ceed fallbacks `willSucceed` silently dropped non-custom-error callStatic failures and collapsed any non-ethers estimateGas error to the literal string `"unknown error"`. That makes upstream telemetry (and the gasless relayer's planned per-failure reporting) much harder to read. Capture the callStatic error and let the eventual reason walk through ethers `reason` → ethers/Error `message` → callStatic `message` → literal "unknown error" only as a last resort. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 979fdd0470
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (estimateGasError instanceof Error && estimateGasError.message) { | ||
| return estimateGasError.message; |
There was a problem hiding this comment.
Prefer callStatic reason before generic gas errors
When callStatic throws a non-custom revert as an Error and the estimateGas fallback throws an ethers UNPREDICTABLE_GAS_LIMIT/generic Error without a populated reason, this branch returns the generic estimateGas.message before ever checking the preserved callStaticError.message. That still drops the more actionable revert context this change is trying to preserve for simulation-failure telemetry; the fallback should only use the estimate-gas message ahead of callStaticError when it is known to be the more specific signal.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in the follow-up commit. Reordered the fallback so an ethers reason on either error still wins, but when neither has one, the callStatic message is preferred over the estimateGas message. Rationale: callStatic is the diagnostic call whose purpose is to surface revert detail, while estimateGas typically yields generic provider noise (UNPREDICTABLE_GAS_LIMIT, "cannot estimate gas; transaction may fail", etc.).
New priority order:
estimateGasError.reason(ethers)callStaticError.reason(ethers)callStaticError.messageestimateGasError.message"unknown error"
Added two regression tests:
prefers callStatic message over a generic estimateGas message when neither carries an ethers reasonprefers an ethers reason on the callStatic error over a callStatic message
When neither error carries an ethers `reason`, prefer the callStatic message over the estimateGas message — callStatic exists to surface revert detail, while estimateGas tends to throw generic provider noise (UNPREDICTABLE_GAS_LIMIT etc.). Also check the callStatic error for an ethers `reason` field before falling back to message strings. Adds two regression tests covering the reordered priority. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Thanks for the review, @codex — addressed the P2 in commit cfde50f. Change: reordered
Previously the generic estimateGas Added two regression tests covering the new ordering. Local check: @codex please re-review. |
|
Codex Review: Didn't find any major issues. Breezy! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Slack discussion: continuation of the gasless stuck-queue reporting thread (PRs #3464, #3465).
willSucceedhad two places where useful error context disappeared:callStaticthrew an error without a customerrorName, the catch block silently fell through to theestimateGasfallback and discarded the underlying error. IfestimateGasthen surfaced something less specific, the original cause was unrecoverable.estimateGaserrors that weren'ttypeguards.isEthersError(e.g. RPC connection issues, raw rejections) collapsed to the literal string"unknown error", even when the thrown value was a perfectly goodErrorwith a populatedmessage.Both made transaction-submission telemetry harder to interpret and prevented the gasless relayer (PR #3464) from carrying a meaningful
reasonin its typedsimulation_failedoutcome.Change
callStaticErrorlocal._extractSimulationReasonhelper that walks:error.reason(preserves existing behavior on Polygon / SDK paths)Error.messagefrom the estimateGas rejectError.messagefrom the captured callStatic error"unknown error"(last-resort sentinel)Behavior unchanged when callStatic reports a custom error name, when estimateGas throws an ethers error with a populated
reason, or when no inputs carry amessage.Doc updates considered
No
AGENTS.md/README.mdshifts.willSucceed's contract —{ succeed, reason }— is unchanged; just the reason content is richer.Test plan
yarn typecheck— clean.yarn lint— clean.yarn hardhat test test/TransactionClient.ts— 15/15 passing, including 5 new cases under "willSucceed simulation-failure reason preservation":reasonwhen estimateGas throws an ethers error (existing behavior)🤖 Generated with Claude Code