diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index ed1da74e8cd..b198e4b080c 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `RetryOnEmptyMiddleware` now correctly propagates execution-revert errors from EIP-1474 / Infura-style providers + - `isExecutionRevertedError` previously required `code: -32000` and an exact `"execution reverted"` message, which never matches the response shape returned by Infura, Alchemy, QuickNode, and other production providers (`code: 3`, `message: "execution reverted: "`) + - The check now also accepts `code: 3` (per EIP-1474) and a message that starts with `"execution reverted"`, preserving backward compatibility with geth-style responses + - Without this fix, every reverted `eth_call` with a numeric block tag was being retried 10 times (~10s of latency) and the original revert payload was discarded behind a generic `"retries exhausted"` error + ## [23.1.2] ### Changed diff --git a/packages/eth-json-rpc-middleware/src/utils/error.test.ts b/packages/eth-json-rpc-middleware/src/utils/error.test.ts index d51eca67272..1fde8dfe564 100644 --- a/packages/eth-json-rpc-middleware/src/utils/error.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/error.test.ts @@ -2,35 +2,60 @@ import { errorCodes } from '@metamask/rpc-errors'; import { isExecutionRevertedError } from './error'; -const executionRevertedError = { +const gethStyleRevert = { code: errorCodes.rpc.invalidInput, message: 'execution reverted', }; +const infuraStyleRevert = { + code: 3, + message: 'execution reverted: ERC20: transfer amount exceeds balance', + data: '0x08c379a0', +}; + describe('isExecutionRevertedError', () => { - it('return false if object is not valid JSON RPC error', async () => { - const result = isExecutionRevertedError({ test: 'dummy' }); - expect(result).toBe(false); + it('returns false if the value is not a valid JSON-RPC error', () => { + expect(isExecutionRevertedError({ test: 'dummy' })).toBe(false); + }); + + it('returns false if the error code is unrelated to reverts', () => { + expect(isExecutionRevertedError({ ...gethStyleRevert, code: 123 })).toBe( + false, + ); + }); + + it('returns false if the error message does not start with "execution reverted"', () => { + expect( + isExecutionRevertedError({ ...gethStyleRevert, message: 'test' }), + ).toBe(false); + }); + + it('returns true for geth-style reverts (code -32000, exact message)', () => { + expect(isExecutionRevertedError(gethStyleRevert)).toBe(true); + }); + + it('returns true for EIP-1474 / Infura-style reverts (code 3, suffixed message)', () => { + expect(isExecutionRevertedError(infuraStyleRevert)).toBe(true); }); - it('return false if error code is not same as errorCodes.rpc.invalidInput', async () => { - const result = isExecutionRevertedError({ - ...executionRevertedError, - code: 123, - }); - expect(result).toBe(false); + it('returns true for code 3 with the bare "execution reverted" message', () => { + expect( + isExecutionRevertedError({ code: 3, message: 'execution reverted' }), + ).toBe(true); }); - it('return false if error message is not "execution reverted"', async () => { - const result = isExecutionRevertedError({ - ...executionRevertedError, - message: 'test', - }); - expect(result).toBe(false); + it('returns true for geth-style reverts with a suffixed message', () => { + expect( + isExecutionRevertedError({ + code: errorCodes.rpc.invalidInput, + message: 'execution reverted: custom reason', + }), + ).toBe(true); }); - it('return true for correct executionRevertedError', async () => { - const result = isExecutionRevertedError(executionRevertedError); - expect(result).toBe(true); + it('returns false when the message is not a string', () => { + expect(isExecutionRevertedError({ ...gethStyleRevert, message: 123 })).toBe( + false, + ); }); }); diff --git a/packages/eth-json-rpc-middleware/src/utils/error.ts b/packages/eth-json-rpc-middleware/src/utils/error.ts index 10da9b81163..40865127857 100644 --- a/packages/eth-json-rpc-middleware/src/utils/error.ts +++ b/packages/eth-json-rpc-middleware/src/utils/error.ts @@ -2,19 +2,48 @@ import { errorCodes } from '@metamask/rpc-errors'; import { isJsonRpcError } from '@metamask/utils'; import type { JsonRpcError } from '@metamask/utils'; +const EXECUTION_REVERTED_PREFIX = 'execution reverted'; + +/** + * EIP-1474 JSON-RPC error code for execution reverts. Returned by Infura + * and most production providers, alongside a message that may carry the + * decoded reason (e.g. `"execution reverted: ERC20: transfer amount + * exceeds balance"`). + * + * @see https://eips.ethereum.org/EIPS/eip-1474 + */ +const EXECUTION_REVERTED_ERROR_CODE = 3; + /** - * Checks if a value is a JSON-RPC error that indicates an execution reverted error. + * Determine whether a JSON-RPC error represents a contract execution revert. + * + * Accepts both: + * - Geth-style: `code: -32000`, `message: "execution reverted"` + * - EIP-1474 / Infura-style: `code: 3`, `message: "execution reverted: "` + * + * Public Infura RPCs and most production providers return the EIP-1474 + * form, so the previous strict check (`code === -32000` and exact + * `"execution reverted"`) never matched real-world reverted responses, + * causing them to be retried by `RetryOnEmptyMiddleware` and the original + * error data to be discarded. * * @param error - The value to check. - * @returns True if the value is a JSON-RPC error that indicates an execution reverted - * error, false otherwise. + * @returns `true` if the error represents an execution revert. */ export function isExecutionRevertedError( error: unknown, ): error is JsonRpcError { + if (!isJsonRpcError(error)) { + return false; + } + + const isExpectedCode = + error.code === errorCodes.rpc.invalidInput || + error.code === EXECUTION_REVERTED_ERROR_CODE; + return ( - isJsonRpcError(error) && - error.code === errorCodes.rpc.invalidInput && - error.message === 'execution reverted' + isExpectedCode && + typeof error.message === 'string' && + error.message.startsWith(EXECUTION_REVERTED_PREFIX) ); }