Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/eth-json-rpc-middleware/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <reason>"`)
- 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
Expand Down
63 changes: 44 additions & 19 deletions packages/eth-json-rpc-middleware/src/utils/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
});
});
41 changes: 35 additions & 6 deletions packages/eth-json-rpc-middleware/src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <reason>"`
*
* 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)
);
}
Loading