From 6fa92969d59c03b4776a13c7b54d0fe905a6c756 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:13:54 -0700 Subject: [PATCH 01/19] refactor: Support JsonRpcEngineV2 --- .../eth-json-rpc-provider/src/index.test.ts | 3 +- .../src/internal-provider.test.ts | 252 +++++++++--------- .../src/internal-provider.ts | 157 +++++++---- .../src/provider-from-engine.test.ts | 4 +- .../src/provider-from-engine.ts | 3 +- .../src/provider-from-middleware.test.ts | 50 +++- .../src/provider-from-middleware.ts | 28 +- 7 files changed, 301 insertions(+), 196 deletions(-) diff --git a/packages/eth-json-rpc-provider/src/index.test.ts b/packages/eth-json-rpc-provider/src/index.test.ts index 1c94f265e9..eb6630252e 100644 --- a/packages/eth-json-rpc-provider/src/index.test.ts +++ b/packages/eth-json-rpc-provider/src/index.test.ts @@ -2,12 +2,13 @@ import * as allExports from '.'; describe('Package exports', () => { it('has expected exports', () => { - expect(Object.keys(allExports)).toMatchInlineSnapshot(` + expect(Object.keys(allExports).sort()).toMatchInlineSnapshot(` Array [ "InternalProvider", "SafeEventEmitterProvider", "providerFromEngine", "providerFromMiddleware", + "providerFromMiddlewareV2", ] `); }); diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index 3e6a897746..3a10a49d3f 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -2,6 +2,7 @@ import { Web3Provider } from '@ethersproject/providers'; import EthQuery from '@metamask/eth-query'; import EthJsQuery from '@metamask/ethjs-query'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2, JsonRpcServer } from '@metamask/json-rpc-engine/v2'; import { providerErrors } from '@metamask/rpc-errors'; import { type JsonRpcRequest, type Json } from '@metamask/utils'; import { BrowserProvider } from 'ethers'; @@ -16,29 +17,44 @@ import { jest.mock('uuid'); -/** - * Creates a mock JSON-RPC engine that returns a predefined response for a specific method. - * - * @param method - The RPC method to mock. - * @param response - The response to return for the mocked method. - * @returns A JSON-RPC engine instance with the mocked method. - */ -function createMockEngine(method: string, response: Json) { +type ResultParam = Json | ((req?: JsonRpcRequest) => Json); + +const createMockEngine = (method: string, result: ResultParam) => { const engine = new JsonRpcEngine(); engine.push((req, res, next, end) => { if (req.method === method) { - res.result = response; + res.result = typeof result === 'function' ? result(req) : result; return end(); } return next(); }); return engine; -} - -describe('InternalProvider', () => { +}; + +const createMockServer = (method: string, result: ResultParam) => { + const engine = JsonRpcEngineV2.create({ + middleware: [ + ({ request, next }) => { + if (request.method === method) { + return typeof result === 'function' + ? result(request as JsonRpcRequest) + : result; + } + return next(); + }, + ], + }); + const server = new JsonRpcServer({ engine }); + return server; +}; + +describe.each([ + { createRpcHandler: createMockEngine, name: 'JsonRpcEngine' }, + { createRpcHandler: createMockServer, name: 'JsonRpcServer' }, +])('InternalProvider with $name', ({ createRpcHandler }) => { it('returns the correct block number with @metamask/eth-query', async () => { const provider = new InternalProvider({ - engine: createMockEngine('eth_blockNumber', 42), + rpcHandler: createRpcHandler('eth_blockNumber', 42), }); const ethQuery = new EthQuery(provider); @@ -49,7 +65,7 @@ describe('InternalProvider', () => { it('returns the correct block number with @metamask/ethjs-query', async () => { const provider = new InternalProvider({ - engine: createMockEngine('eth_blockNumber', 42), + rpcHandler: createRpcHandler('eth_blockNumber', 42), }); const ethJsQuery = new EthJsQuery(provider); @@ -60,7 +76,7 @@ describe('InternalProvider', () => { it('returns the correct block number with Web3Provider', async () => { const provider = new InternalProvider({ - engine: createMockEngine('eth_blockNumber', 42), + rpcHandler: createRpcHandler('eth_blockNumber', 42), }); const web3Provider = new Web3Provider(provider); @@ -71,26 +87,26 @@ describe('InternalProvider', () => { it('returns the correct block number with BrowserProvider', async () => { const provider = new InternalProvider({ - engine: createMockEngine('eth_blockNumber', 42), + rpcHandler: createRpcHandler('eth_blockNumber', 42), }); const browserProvider = new BrowserProvider(provider); const response = await browserProvider.send('eth_blockNumber', []); expect(response).toBe(42); + + browserProvider.destroy(); }); describe('request', () => { it('handles a successful JSON-RPC object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); - const exampleRequest = { + const provider = new InternalProvider({ rpcHandler }); + const request = { id: 1, jsonrpc: '2.0' as const, method: 'test', @@ -100,10 +116,10 @@ describe('InternalProvider', () => { }, }; - const result = await provider.request(exampleRequest); + const result = await provider.request(request); expect(req).toStrictEqual({ - id: 1, + id: expect.anything(), jsonrpc: '2.0' as const, method: 'test', params: { @@ -115,15 +131,13 @@ describe('InternalProvider', () => { }); it('handles a successful EIP-1193 object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); - const exampleRequest = { + const provider = new InternalProvider({ rpcHandler }); + const request = { method: 'test', params: { param1: 'value1', @@ -132,10 +146,10 @@ describe('InternalProvider', () => { }; jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - const result = await provider.request(exampleRequest); + const result = await provider.request(request); expect(req).toStrictEqual({ - id: 'mock-id', + id: expect.anything(), jsonrpc: '2.0' as const, method: 'test', params: { @@ -147,28 +161,21 @@ describe('InternalProvider', () => { }); it('handles a failure with a non-JSON-RPC error', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, _res, _next, end) => { - end( - providerErrors.custom({ - code: 1001, - message: 'Test error', - data: { - cause: 'Test cause', - }, - }), - ); + const rpcHandler = createRpcHandler('test', () => { + throw providerErrors.custom({ + code: 1001, + message: 'Test error', + data: { cause: 'Test cause' }, + }); }); - const provider = new InternalProvider({ engine }); - const exampleRequest = { + const provider = new InternalProvider({ rpcHandler }); + const request = { id: 1, jsonrpc: '2.0' as const, method: 'test', }; - await expect(async () => - provider.request(exampleRequest), - ).rejects.toThrow( + await expect(async () => provider.request(request)).rejects.toThrow( expect.objectContaining({ code: 1001, message: 'Test error', @@ -179,20 +186,17 @@ describe('InternalProvider', () => { }); it('handles a failure with a JSON-RPC error', async () => { - const engine = new JsonRpcEngine(); - engine.push(() => { + const rpcHandler = createRpcHandler('test', () => { throw new Error('Test error'); }); - const provider = new InternalProvider({ engine }); - const exampleRequest = { + const provider = new InternalProvider({ rpcHandler }); + const request = { id: 1, jsonrpc: '2.0' as const, method: 'test', }; - await expect(async () => - provider.request(exampleRequest), - ).rejects.toThrow( + await expect(async () => provider.request(request)).rejects.toThrow( expect.objectContaining({ code: -32603, message: 'Test error', @@ -209,16 +213,14 @@ describe('InternalProvider', () => { describe('sendAsync', () => { it('handles a successful JSON-RPC object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); - const exampleRequest = { + const request = { id: 1, jsonrpc: '2.0' as const, method: 'test', @@ -228,10 +230,10 @@ describe('InternalProvider', () => { }, }; - const response = await promisifiedSendAsync(exampleRequest); + const response = await promisifiedSendAsync(request); expect(req).toStrictEqual({ - id: 1, + id: expect.anything(), jsonrpc: '2.0' as const, method: 'test', params: { @@ -243,16 +245,14 @@ describe('InternalProvider', () => { }); it('handles a successful EIP-1193 object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); - const exampleRequest = { + const request = { method: 'test', params: { param1: 'value1', @@ -261,10 +261,10 @@ describe('InternalProvider', () => { }; jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - const response = await promisifiedSendAsync(exampleRequest); + const response = await promisifiedSendAsync(request); expect(req).toStrictEqual({ - id: 'mock-id', + id: expect.anything(), jsonrpc: '2.0' as const, method: 'test', params: { @@ -276,50 +276,66 @@ describe('InternalProvider', () => { }); it('handles a failed request', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, _res, _next, _end) => { + const rpcHandler = createRpcHandler('test', () => { throw new Error('Test error'); }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); - const exampleRequest = { + const request = { id: 1, jsonrpc: '2.0' as const, method: 'test', }; - await expect(async () => - promisifiedSendAsync(exampleRequest), - ).rejects.toThrow('Test error'); + await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( + 'Test error', + ); + }); + + it('handles an error thrown by the JSON-RPC handler', async () => { + const rpcHandler = createRpcHandler('test', () => null); + jest + .spyOn(rpcHandler, 'handle') + .mockRejectedValue(new Error('Test error')); + const provider = new InternalProvider({ rpcHandler }); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( + 'Test error', + ); }); }); describe('send', () => { it('throws if a callback is not provided', () => { - const engine = new JsonRpcEngine(); - const provider = new InternalProvider({ engine }); - const exampleRequest = { + const rpcHandler = createRpcHandler('test', 42); + const provider = new InternalProvider({ rpcHandler }); + const request = { id: 1, jsonrpc: '2.0' as const, method: 'test', }; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(() => (provider.send as any)(exampleRequest)).toThrow(''); + // @ts-expect-error - Destructive testing. + expect(() => provider.send(request)).toThrow( + 'Must provide callback to "send" method.', + ); }); it('handles a successful JSON-RPC object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ rpcHandler }); const promisifiedSend = promisify(provider.send); - const exampleRequest = { + const request = { id: 1, jsonrpc: '2.0' as const, method: 'test', @@ -329,10 +345,10 @@ describe('InternalProvider', () => { }, }; - const response = await promisifiedSend(exampleRequest); + const response = await promisifiedSend(request); expect(req).toStrictEqual({ - id: 1, + id: expect.anything(), jsonrpc: '2.0' as const, method: 'test', params: { @@ -344,16 +360,14 @@ describe('InternalProvider', () => { }); it('handles a successful EIP-1193 object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ rpcHandler }); const promisifiedSend = promisify(provider.send); - const exampleRequest = { + const request = { method: 'test', params: { param1: 'value1', @@ -362,10 +376,10 @@ describe('InternalProvider', () => { }; jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - const response = await promisifiedSend(exampleRequest); + const response = await promisifiedSend(request); expect(req).toStrictEqual({ - id: 'mock-id', + id: expect.anything(), jsonrpc: '2.0' as const, method: 'test', params: { @@ -377,19 +391,18 @@ describe('InternalProvider', () => { }); it('handles a failed request', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, _res, _next, _end) => { + const rpcHandler = createRpcHandler('test', () => { throw new Error('Test error'); }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ rpcHandler }); const promisifiedSend = promisify(provider.send); - const exampleRequest = { + const request = { id: 1, jsonrpc: '2.0' as const, method: 'test', }; - await expect(async () => promisifiedSend(exampleRequest)).rejects.toThrow( + await expect(async () => promisifiedSend(request)).rejects.toThrow( 'Test error', ); }); @@ -415,23 +428,6 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { }); }); - it('uses the provided id if id is provided', () => { - const eip1193Request = { - id: '123', - method: 'test', - params: { param1: 'value1', param2: 'value2' }, - }; - const jsonRpcRequest = - convertEip1193RequestToJsonRpcRequest(eip1193Request); - - expect(jsonRpcRequest).toStrictEqual({ - id: '123', - jsonrpc: '2.0', - method: 'test', - params: { param1: 'value1', param2: 'value2' }, - }); - }); - it('uses the default jsonrpc version if not provided', () => { jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index 9c1a59c0df..5f77a3dc02 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -1,11 +1,15 @@ import type { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; import { JsonRpcError } from '@metamask/rpc-errors'; -import type { - Json, - JsonRpcId, - JsonRpcParams, - JsonRpcRequest, - JsonRpcVersion2, +import type { JsonRpcFailure } from '@metamask/utils'; +import { + hasProperty, + type Json, + type JsonRpcId, + type JsonRpcParams, + type JsonRpcRequest, + type JsonRpcResponse, + type JsonRpcVersion2, } from '@metamask/utils'; import { v4 as uuidV4 } from 'uuid'; @@ -19,31 +23,16 @@ type Eip1193Request = { params?: Params; }; -/** - * Converts an EIP-1193 request to a JSON-RPC request. - * - * @param eip1193Request - The EIP-1193 request to convert. - * @returns The corresponding JSON-RPC request. - */ -export function convertEip1193RequestToJsonRpcRequest< - Params extends JsonRpcParams, ->( - eip1193Request: Eip1193Request, -): JsonRpcRequest> { - const { id = uuidV4(), jsonrpc = '2.0', method, params } = eip1193Request; - return params - ? { - id, - jsonrpc, - method, - params, - } - : { - id, - jsonrpc, - method, - }; -} +type Options = + | { + /** + * @deprecated Use `rpcHandler` instead. + */ + engine: JsonRpcEngine; + } + | { + rpcHandler: JsonRpcEngine | JsonRpcServer; + }; /** * An Ethereum provider. @@ -52,16 +41,18 @@ export function convertEip1193RequestToJsonRpcRequest< * It is not compliant with any Ethereum provider standard. */ export class InternalProvider { - readonly #engine: JsonRpcEngine; + readonly #rpcHandler: JsonRpcEngine | JsonRpcServer; /** - * Construct a InternalProvider from a JSON-RPC engine. + * Construct a InternalProvider from a JSON-RPC server or legacy engine. * * @param options - Options. - * @param options.engine - The JSON-RPC engine used to process requests. + * @param options.rpcHandler - The JSON-RPC server or engine used to process requests. Mutually exclusive with `engine`. + * @param options.engine - The JSON-RPC engine used to process requests. Mutually exclusive with `rpcHandler`. */ - constructor({ engine }: { engine: JsonRpcEngine }) { - this.#engine = engine; + constructor(options: Options) { + this.#rpcHandler = + 'rpcHandler' in options ? options.rpcHandler : options.engine; } /** @@ -75,24 +66,13 @@ export class InternalProvider { ): Promise { const jsonRpcRequest = convertEip1193RequestToJsonRpcRequest(eip1193Request); - const response = await this.#engine.handle< - Params | Record, - Result - >(jsonRpcRequest); + const response: JsonRpcResponse = + await this.#handle(jsonRpcRequest); if ('result' in response) { return response.result; } - - const error = new JsonRpcError( - response.error.code, - response.error.message, - response.error.data, - ); - if ('stack' in response.error) { - error.stack = response.error.stack; - } - throw error; + throw deserializeError(response.error); } /** @@ -103,17 +83,17 @@ export class InternalProvider { * * @param eip1193Request - The request to send. * @param callback - A function that is called upon the success or failure of the request. - * @deprecated Please use `request` instead. + * @deprecated Use {@link request} instead. */ sendAsync = ( eip1193Request: Eip1193Request, - // TODO: Replace `any` with type + // Non-polluting `any` that acts like a constraint. // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (error: unknown, providerRes?: any) => void, ) => { const jsonRpcRequest = convertEip1193RequestToJsonRpcRequest(eip1193Request); - this.#engine.handle(jsonRpcRequest, callback); + this.#handleWithCallback(jsonRpcRequest, callback); }; /** @@ -124,7 +104,7 @@ export class InternalProvider { * * @param eip1193Request - The request to send. * @param callback - A function that is called upon the success or failure of the request. - * @deprecated Please use `request` instead. + * @deprecated Use {@link request} instead. */ send = ( eip1193Request: Eip1193Request, @@ -137,6 +117,73 @@ export class InternalProvider { } const jsonRpcRequest = convertEip1193RequestToJsonRpcRequest(eip1193Request); - this.#engine.handle(jsonRpcRequest, callback); + this.#handleWithCallback(jsonRpcRequest, callback); + }; + + readonly #handle = async ( + jsonRpcRequest: JsonRpcRequest, + ): Promise> => { + // @ts-expect-error - The signatures are incompatible between the legacy engine + // and server, but this works at runtime. + return await this.#rpcHandler.handle(jsonRpcRequest); + }; + + readonly #handleWithCallback = ( + jsonRpcRequest: JsonRpcRequest, + callback: (error: unknown, providerRes?: unknown) => void, + ): void => { + /* eslint-disable promise/always-return,promise/no-callback-in-promise */ + this.#handle(jsonRpcRequest) + .then((response) => { + if (hasProperty(response, 'result')) { + callback(null, response); + } else { + callback(deserializeError(response.error)); + } + }) + .catch((error) => { + callback(error); + }); + /* eslint-enable promise/always-return,promise/no-callback-in-promise */ }; } + +/** + * Convert an EIP-1193 request to a JSON-RPC request. + * + * @param eip1193Request - The EIP-1193 request to convert. + * @returns The JSON-RPC request. + */ +export function convertEip1193RequestToJsonRpcRequest< + Params extends JsonRpcParams, +>( + eip1193Request: Eip1193Request, +): JsonRpcRequest> { + const { id = uuidV4(), jsonrpc = '2.0', method, params } = eip1193Request; + return params + ? { + id, + jsonrpc, + method, + params, + } + : { + id, + jsonrpc, + method, + }; +} + +/** + * Deserialize a JSON-RPC error. + * + * @param error - The JSON-RPC error to deserialize. + * @returns The deserialized error. + */ +function deserializeError(error: JsonRpcFailure['error']): JsonRpcError { + const jsonRpcError = new JsonRpcError(error.code, error.message, error.data); + if ('stack' in error) { + jsonRpcError.stack = error.stack; + } + return jsonRpcError; +} diff --git a/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts b/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts index ca90a1deac..6e7589aa7b 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts @@ -4,7 +4,7 @@ import { providerErrors } from '@metamask/rpc-errors'; import { providerFromEngine } from './provider-from-engine'; describe('providerFromEngine', () => { - it('handle a successful request', async () => { + it('handles a successful request', async () => { const engine = new JsonRpcEngine(); engine.push((_req, res, _next, end) => { res.result = 42; @@ -22,7 +22,7 @@ describe('providerFromEngine', () => { expect(response).toBe(42); }); - it('handle a failed request', async () => { + it('handles a failed request', async () => { const engine = new JsonRpcEngine(); engine.push((_req, _res, _next, end) => { end( diff --git a/packages/eth-json-rpc-provider/src/provider-from-engine.ts b/packages/eth-json-rpc-provider/src/provider-from-engine.ts index 0f90662507..d8800def44 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-engine.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-engine.ts @@ -3,10 +3,11 @@ import type { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { InternalProvider } from './internal-provider'; /** - * Construct an Ethereum provider from the given JSON-RPC engine. + * Construct an Ethereum provider from a JSON-RPC engine. * * @param engine - The JSON-RPC engine to construct a provider from. * @returns An Ethereum provider. + * @deprecated Just use {@link InternalProvider} directly instead. */ export function providerFromEngine(engine: JsonRpcEngine): InternalProvider { return new InternalProvider({ engine }); diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.test.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.test.ts index c3e73d3153..6c20713e0b 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.test.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.test.ts @@ -1,13 +1,21 @@ -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware as LegacyJsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import { providerErrors } from '@metamask/rpc-errors'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; -import { providerFromMiddleware } from './provider-from-middleware'; +import { + providerFromMiddleware, + providerFromMiddlewareV2, +} from './provider-from-middleware'; describe('providerFromMiddleware', () => { it('handle a successful request', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const middleware: JsonRpcMiddleware = (_req, res, _next, end) => { + const middleware: LegacyJsonRpcMiddleware = ( + _req, + res, + _next, + end, + ) => { res.result = 42; end(); }; @@ -43,3 +51,35 @@ describe('providerFromMiddleware', () => { ); }); }); + +describe('providerFromMiddlewareV2', () => { + it('handle a successful request', async () => { + const middleware: JsonRpcMiddleware = () => 42; + const provider = providerFromMiddlewareV2(middleware); + const exampleRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + const response = await provider.request(exampleRequest); + + expect(response).toBe(42); + }); + + it('handle a failed request', async () => { + const middleware: JsonRpcMiddleware = () => { + throw new Error('Test error'); + }; + const provider = providerFromMiddlewareV2(middleware); + const exampleRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => provider.request(exampleRequest)).rejects.toThrow( + 'Test error', + ); + }); +}); diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts index e77d487464..f0bc22dc1e 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts @@ -1,8 +1,10 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcParams } from '@metamask/utils'; +import type { JsonRpcMiddleware as LegacyJsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; -import type { InternalProvider } from './internal-provider'; +import { InternalProvider } from './internal-provider'; import { providerFromEngine } from './provider-from-engine'; /** @@ -10,13 +12,31 @@ import { providerFromEngine } from './provider-from-engine'; * * @param middleware - The middleware to construct a provider from. * @returns An Ethereum provider. + * @deprecated Use {@link providerFromMiddlewareV2} instead. */ export function providerFromMiddleware< Params extends JsonRpcParams, Result extends Json, ->(middleware: JsonRpcMiddleware): InternalProvider { +>(middleware: LegacyJsonRpcMiddleware): InternalProvider { const engine: JsonRpcEngine = new JsonRpcEngine(); engine.push(middleware); const provider: InternalProvider = providerFromEngine(engine); return provider; } + +/** + * Construct an Ethereum provider from the given middleware. + * + * @param middleware - The middleware to construct a provider from. + * @returns An Ethereum provider. + */ +export function providerFromMiddlewareV2< + Params extends JsonRpcParams, + Result extends Json, +>( + middleware: JsonRpcMiddleware, Result>, +): InternalProvider { + return new InternalProvider({ + rpcHandler: new JsonRpcServer({ middleware: [middleware] }), + }); +} From 7c06c59301c29f49a469375df82a9033d4ced577 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:00:51 -0700 Subject: [PATCH 02/19] test: Add missing test case --- .../src/internal-provider.test.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index 3a10a49d3f..093a6065c5 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -19,7 +19,7 @@ jest.mock('uuid'); type ResultParam = Json | ((req?: JsonRpcRequest) => Json); -const createMockEngine = (method: string, result: ResultParam) => { +const createEngine = (method: string, result: ResultParam) => { const engine = new JsonRpcEngine(); engine.push((req, res, next, end) => { if (req.method === method) { @@ -31,7 +31,7 @@ const createMockEngine = (method: string, result: ResultParam) => { return engine; }; -const createMockServer = (method: string, result: ResultParam) => { +const createServer = (method: string, result: ResultParam) => { const engine = JsonRpcEngineV2.create({ middleware: [ ({ request, next }) => { @@ -48,9 +48,18 @@ const createMockServer = (method: string, result: ResultParam) => { return server; }; +describe('legacy constructor', () => { + it('can be constructed with an engine', () => { + const provider = new InternalProvider({ + engine: createEngine('eth_blockNumber', 42), + }); + expect(provider).toBeDefined(); + }); +}); + describe.each([ - { createRpcHandler: createMockEngine, name: 'JsonRpcEngine' }, - { createRpcHandler: createMockServer, name: 'JsonRpcServer' }, + { createRpcHandler: createEngine, name: 'JsonRpcEngine' }, + { createRpcHandler: createServer, name: 'JsonRpcServer' }, ])('InternalProvider with $name', ({ createRpcHandler }) => { it('returns the correct block number with @metamask/eth-query', async () => { const provider = new InternalProvider({ From 047f8151de8b861bdd937527b85b4de4009c9823 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:02:22 -0700 Subject: [PATCH 03/19] docs: Update changelog --- packages/eth-json-rpc-provider/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index f108a4df33..90a38cb90f 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Replace `SafeEventEmitterProvider` with `InternalProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - The new class is behaviorally equivalent to the previous version except it does not extend `SafeEventEmitter`. - `SafeEventEmitterProvider` is for now still exported as a deprecated alias of `InternalProvider` for backwards compatibility. +- Support constructing `InternalProvider` with a `JsonRpcServer` instance ([#7001](https://github.com/MetaMask/core/pull/7001)) + - The `rpcHandler` constructor option accepts either a `JsonRpcServer` or a legacy `JsonRpcEngine`. It is mutually exclusive with the `engine` option. ## [5.0.1] From 5893198e439fe4fbc27e72dc14f1b2319cc92330 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:25:27 -0700 Subject: [PATCH 04/19] fix: Ensure json-rpc-engine/v2 can be resolved without builds --- jest.config.packages.js | 3 +++ .../src/internal-provider.test.ts | 6 +++--- .../eth-json-rpc-provider/src/internal-provider.ts | 13 ++++++++++--- .../src/provider-from-middleware.ts | 7 ++----- tsconfig.packages.json | 1 + 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/jest.config.packages.js b/jest.config.packages.js index ce6f1f267d..b3406afaf1 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -80,6 +80,9 @@ module.exports = { // Here we ensure that Jest resolves `@metamask/*` imports to the uncompiled source code for packages that live in this repo. // NOTE: This must be synchronized with the `paths` option in `tsconfig.packages.json`. moduleNameMapper: { + '^@metamask/json-rpc-engine/v2$': [ + '/../json-rpc-engine/src/v2/index.ts', + ], '^@metamask/(.+)$': [ '/../$1/src', // Some @metamask/* packages we are referencing aren't in this monorepo, diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index 093a6065c5..17f2c9b809 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -2,6 +2,7 @@ import { Web3Provider } from '@ethersproject/providers'; import EthQuery from '@metamask/eth-query'; import EthJsQuery from '@metamask/ethjs-query'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import { JsonRpcEngineV2, JsonRpcServer } from '@metamask/json-rpc-engine/v2'; import { providerErrors } from '@metamask/rpc-errors'; import { type JsonRpcRequest, type Json } from '@metamask/utils'; @@ -32,7 +33,7 @@ const createEngine = (method: string, result: ResultParam) => { }; const createServer = (method: string, result: ResultParam) => { - const engine = JsonRpcEngineV2.create({ + const engine = JsonRpcEngineV2.create>({ middleware: [ ({ request, next }) => { if (request.method === method) { @@ -44,8 +45,7 @@ const createServer = (method: string, result: ResultParam) => { }, ], }); - const server = new JsonRpcServer({ engine }); - return server; + return new JsonRpcServer>({ engine }); }; describe('legacy constructor', () => { diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index 5f77a3dc02..3b52e5d56e 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -1,5 +1,8 @@ import type { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; +import type { + JsonRpcMiddleware, + JsonRpcServer, +} from '@metamask/json-rpc-engine/v2'; import { JsonRpcError } from '@metamask/rpc-errors'; import type { JsonRpcFailure } from '@metamask/utils'; import { @@ -31,7 +34,9 @@ type Options = engine: JsonRpcEngine; } | { - rpcHandler: JsonRpcEngine | JsonRpcServer; + rpcHandler: + | JsonRpcEngine + | JsonRpcServer>; }; /** @@ -41,7 +46,9 @@ type Options = * It is not compliant with any Ethereum provider standard. */ export class InternalProvider { - readonly #rpcHandler: JsonRpcEngine | JsonRpcServer; + readonly #rpcHandler: + | JsonRpcEngine + | JsonRpcServer>; /** * Construct a InternalProvider from a JSON-RPC server or legacy engine. diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts index f0bc22dc1e..22f5548122 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts @@ -31,11 +31,8 @@ export function providerFromMiddleware< * @returns An Ethereum provider. */ export function providerFromMiddlewareV2< - Params extends JsonRpcParams, - Result extends Json, ->( - middleware: JsonRpcMiddleware, Result>, -): InternalProvider { + Middleware extends JsonRpcMiddleware, +>(middleware: Middleware): InternalProvider { return new InternalProvider({ rpcHandler: new JsonRpcServer({ middleware: [middleware] }), }); diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 8d0d5aee5e..b02edfcb6b 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -13,6 +13,7 @@ * `jest.config.packages.js`. */ "paths": { + "@metamask/json-rpc-engine/v2": ["../json-rpc-engine/src/v2/index.ts"], "@metamask/*": ["../*/src"] }, "strict": true, From 560f6b2b67e742d6c07a99d6708d2360a8027938 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:03:06 -0700 Subject: [PATCH 05/19] refactor(provider): prefer v2 JsonRpcServer; wrap legacy engines; tighten v2 types - InternalProvider - replace rpcHandler with server - wrap legacy engine via asV2Middleware + JsonRpcServer - update tests - asV2Middleware: fix middleware signature - providerFromMiddleware - Use providerFromMiddlewareV2 and asV2Middleware to wrap legacy middleware - json-rpc-engine: - add MiddlewareConstraint - update JsonRpcServer/asV2Middleware generics --- .../src/internal-provider.test.ts | 679 ++++++++++-------- .../src/internal-provider.ts | 61 +- .../src/provider-from-middleware.ts | 18 +- .../json-rpc-engine/src/asV2Middleware.ts | 7 +- .../json-rpc-engine/src/v2/JsonRpcEngineV2.ts | 16 +- .../json-rpc-engine/src/v2/JsonRpcServer.ts | 13 +- 6 files changed, 424 insertions(+), 370 deletions(-) diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index 17f2c9b809..979a0825ea 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -1,10 +1,10 @@ import { Web3Provider } from '@ethersproject/providers'; import EthQuery from '@metamask/eth-query'; import EthJsQuery from '@metamask/ethjs-query'; -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { asV2Middleware, JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import { JsonRpcEngineV2, JsonRpcServer } from '@metamask/json-rpc-engine/v2'; -import { providerErrors } from '@metamask/rpc-errors'; +import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { type JsonRpcRequest, type Json } from '@metamask/utils'; import { BrowserProvider } from 'ethers'; import { promisify } from 'util'; @@ -57,366 +57,409 @@ describe('legacy constructor', () => { }); }); +const createOptions = ( + paramName: 'engine' | 'server', + rpcHandler: ReturnType, +) => + ({ + [paramName]: rpcHandler, + }) as ConstructorParameters[0]; + describe.each([ - { createRpcHandler: createEngine, name: 'JsonRpcEngine' }, - { createRpcHandler: createServer, name: 'JsonRpcServer' }, -])('InternalProvider with $name', ({ createRpcHandler }) => { - it('returns the correct block number with @metamask/eth-query', async () => { - const provider = new InternalProvider({ - rpcHandler: createRpcHandler('eth_blockNumber', 42), - }); - const ethQuery = new EthQuery(provider); + { + createRpcHandler: createEngine, + name: 'JsonRpcEngine', + paramName: 'engine', + }, + { + createRpcHandler: createServer, + name: 'JsonRpcServer', + paramName: 'server', + }, +] as const)( + 'InternalProvider with $name', + ({ createRpcHandler, paramName }) => { + it('returns the correct block number with @metamask/eth-query', async () => { + const provider = new InternalProvider( + createOptions(paramName, createRpcHandler('eth_blockNumber', 42)), + ); + const ethQuery = new EthQuery(provider); - ethQuery.sendAsync({ method: 'eth_blockNumber' }, (_error, response) => { - expect(response).toBe(42); + ethQuery.sendAsync({ method: 'eth_blockNumber' }, (_error, response) => { + expect(response).toBe(42); + }); }); - }); - it('returns the correct block number with @metamask/ethjs-query', async () => { - const provider = new InternalProvider({ - rpcHandler: createRpcHandler('eth_blockNumber', 42), + it('returns the correct block number with @metamask/ethjs-query', async () => { + const provider = new InternalProvider( + createOptions(paramName, createRpcHandler('eth_blockNumber', 42)), + ); + const ethJsQuery = new EthJsQuery(provider); + + const response = await ethJsQuery.blockNumber(); + + expect(response.toNumber()).toBe(42); }); - const ethJsQuery = new EthJsQuery(provider); - const response = await ethJsQuery.blockNumber(); + it('returns the correct block number with Web3Provider', async () => { + const provider = new InternalProvider( + createOptions(paramName, createRpcHandler('eth_blockNumber', 42)), + ); + const web3Provider = new Web3Provider(provider); - expect(response.toNumber()).toBe(42); - }); + const response = await web3Provider.send('eth_blockNumber', []); - it('returns the correct block number with Web3Provider', async () => { - const provider = new InternalProvider({ - rpcHandler: createRpcHandler('eth_blockNumber', 42), + expect(response).toBe(42); }); - const web3Provider = new Web3Provider(provider); - const response = await web3Provider.send('eth_blockNumber', []); + it('returns the correct block number with BrowserProvider', async () => { + const provider = new InternalProvider( + createOptions(paramName, createRpcHandler('eth_blockNumber', 42)), + ); + const browserProvider = new BrowserProvider(provider); - expect(response).toBe(42); - }); + const response = await browserProvider.send('eth_blockNumber', []); - it('returns the correct block number with BrowserProvider', async () => { - const provider = new InternalProvider({ - rpcHandler: createRpcHandler('eth_blockNumber', 42), - }); - const browserProvider = new BrowserProvider(provider); + expect(response).toBe(42); - const response = await browserProvider.send('eth_blockNumber', []); + browserProvider.destroy(); + }); - expect(response).toBe(42); + describe('request', () => { + it('handles a successful JSON-RPC object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; - browserProvider.destroy(); - }); + const result = await provider.request(request); - describe('request', () => { - it('handles a successful JSON-RPC object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider({ rpcHandler }); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - - const result = await provider.request(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, + expect(req).toStrictEqual({ + id: expect.anything(), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(result).toBe(42); }); - expect(result).toBe(42); - }); - it('handles a successful EIP-1193 object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; + it('handles a successful EIP-1193 object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const request = { + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); + + const result = await provider.request(request); + + expect(req).toStrictEqual({ + id: expect.anything(), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(result).toBe(42); }); - const provider = new InternalProvider({ rpcHandler }); - const request = { - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - - const result = await provider.request(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, + + it('handles a failure with a non-JSON-RPC error', async () => { + const rpcHandler = createRpcHandler('test', () => { + throw providerErrors.custom({ + code: 1001, + message: 'Test error', + data: { cause: 'Test cause' }, + }); + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => provider.request(request)).rejects.toThrow( + providerErrors.custom({ + code: 1001, + message: 'Test error', + data: { cause: 'Test cause' }, + }), + ); }); - expect(result).toBe(42); - }); - it('handles a failure with a non-JSON-RPC error', async () => { - const rpcHandler = createRpcHandler('test', () => { - throw providerErrors.custom({ - code: 1001, - message: 'Test error', - data: { cause: 'Test cause' }, + it('handles a failure with a JSON-RPC error', async () => { + const rpcHandler = createRpcHandler('test', () => { + throw new Error('Test error'); }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => provider.request(request)).rejects.toThrow( + rpcErrors.internal({ + message: 'Test error', + data: { cause: 'Test cause' }, + }), + ); }); - const provider = new InternalProvider({ rpcHandler }); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => provider.request(request)).rejects.toThrow( - expect.objectContaining({ - code: 1001, - message: 'Test error', - data: { cause: 'Test cause' }, - stack: expect.stringContaining('internal-provider.test.ts:'), - }), - ); }); - it('handles a failure with a JSON-RPC error', async () => { - const rpcHandler = createRpcHandler('test', () => { - throw new Error('Test error'); - }); - const provider = new InternalProvider({ rpcHandler }); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => provider.request(request)).rejects.toThrow( - expect.objectContaining({ - code: -32603, - message: 'Test error', - data: { - cause: expect.objectContaining({ - stack: expect.stringContaining('internal-provider.test.ts:'), - message: 'Test error', - }), + describe('sendAsync', () => { + it('handles a successful JSON-RPC object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', }, - }), - ); - }); - }); + }; - describe('sendAsync', () => { - it('handles a successful JSON-RPC object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; + const response = await promisifiedSendAsync(request); + + expect(req).toStrictEqual({ + id: expect.anything(), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(response.result).toBe(42); }); - const provider = new InternalProvider({ rpcHandler }); - const promisifiedSendAsync = promisify(provider.sendAsync); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - - const response = await promisifiedSendAsync(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, + + it('handles a successful EIP-1193 object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); + + const response = await promisifiedSendAsync(request); + + expect(req).toStrictEqual({ + id: expect.anything(), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(response.result).toBe(42); }); - expect(response.result).toBe(42); - }); - it('handles a successful EIP-1193 object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; + it('handles a failed request', async () => { + const rpcHandler = createRpcHandler('test', () => { + throw new Error('Test error'); + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( + 'Test error', + ); }); - const provider = new InternalProvider({ rpcHandler }); - const promisifiedSendAsync = promisify(provider.sendAsync); - const request = { - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - - const response = await promisifiedSendAsync(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, + + it('handles an error thrown by the JSON-RPC handler', async () => { + let rpcHandler = createRpcHandler('test', () => null); + // Transform the engine into a server so we can mock the "handle" method. + // The "handle" method should never throw, but we should be resilient to it anyway. + rpcHandler = + // eslint-disable-next-line jest/no-conditional-in-test + 'push' in rpcHandler + ? new JsonRpcServer({ middleware: [asV2Middleware(rpcHandler)] }) + : rpcHandler; + jest + .spyOn(rpcHandler, 'handle') + .mockRejectedValue(new Error('Test error')); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( + 'Test error', + ); }); - expect(response.result).toBe(42); }); - it('handles a failed request', async () => { - const rpcHandler = createRpcHandler('test', () => { - throw new Error('Test error'); + describe('send', () => { + it('throws if a callback is not provided', () => { + const rpcHandler = createRpcHandler('test', 42); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + // @ts-expect-error - Destructive testing. + expect(() => provider.send(request)).toThrow( + 'Must provide callback to "send" method.', + ); }); - const provider = new InternalProvider({ rpcHandler }); - const promisifiedSendAsync = promisify(provider.sendAsync); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( - 'Test error', - ); - }); - it('handles an error thrown by the JSON-RPC handler', async () => { - const rpcHandler = createRpcHandler('test', () => null); - jest - .spyOn(rpcHandler, 'handle') - .mockRejectedValue(new Error('Test error')); - const provider = new InternalProvider({ rpcHandler }); - const promisifiedSendAsync = promisify(provider.sendAsync); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( - 'Test error', - ); - }); - }); + it('handles a successful JSON-RPC object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const promisifiedSend = promisify(provider.send); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; - describe('send', () => { - it('throws if a callback is not provided', () => { - const rpcHandler = createRpcHandler('test', 42); - const provider = new InternalProvider({ rpcHandler }); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - // @ts-expect-error - Destructive testing. - expect(() => provider.send(request)).toThrow( - 'Must provide callback to "send" method.', - ); - }); + const response = await promisifiedSend(request); - it('handles a successful JSON-RPC object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider({ rpcHandler }); - const promisifiedSend = promisify(provider.send); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - - const response = await promisifiedSend(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, + expect(req).toStrictEqual({ + id: expect.anything(), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(response.result).toBe(42); }); - expect(response.result).toBe(42); - }); - it('handles a successful EIP-1193 object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider({ rpcHandler }); - const promisifiedSend = promisify(provider.send); - const request = { - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - - const response = await promisifiedSend(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, + it('handles a successful EIP-1193 object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const promisifiedSend = promisify(provider.send); + const request = { + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); + + const response = await promisifiedSend(request); + + expect(req).toStrictEqual({ + id: expect.anything(), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(response.result).toBe(42); }); - expect(response.result).toBe(42); - }); - it('handles a failed request', async () => { - const rpcHandler = createRpcHandler('test', () => { - throw new Error('Test error'); + it('handles a failed request', async () => { + const rpcHandler = createRpcHandler('test', () => { + throw new Error('Test error'); + }); + const provider = new InternalProvider( + createOptions(paramName, rpcHandler), + ); + const promisifiedSend = promisify(provider.send); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => promisifiedSend(request)).rejects.toThrow( + 'Test error', + ); }); - const provider = new InternalProvider({ rpcHandler }); - const promisifiedSend = promisify(provider.send); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => promisifiedSend(request)).rejects.toThrow( - 'Test error', - ); }); - }); -}); + }, +); describe('convertEip1193RequestToJsonRpcRequest', () => { it('generates a unique id if id is not provided', () => { diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index 3b52e5d56e..e5bba86b8b 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -1,8 +1,6 @@ -import type { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { - JsonRpcMiddleware, - JsonRpcServer, -} from '@metamask/json-rpc-engine/v2'; +import { asV2Middleware, type JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; import { JsonRpcError } from '@metamask/rpc-errors'; import type { JsonRpcFailure } from '@metamask/utils'; import { @@ -26,17 +24,28 @@ type Eip1193Request = { params?: Params; }; -type Options = +/** + * The {@link JsonRpcMiddleware} constraint and default type for the {@link InternalProvider}. + * We care that the middleware can handle JSON-RPC requests, but do not care about the context, + * the validity of which is enforced by the {@link JsonRpcServer}. + */ +export type InternalProviderMiddleware = JsonRpcMiddleware< + JsonRpcRequest, + Json, + // Non-polluting `any` constraint. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any +>; + +type Options = | { /** - * @deprecated Use `rpcHandler` instead. + * @deprecated Use `server` instead. */ engine: JsonRpcEngine; } | { - rpcHandler: - | JsonRpcEngine - | JsonRpcServer>; + server: JsonRpcServer; }; /** @@ -45,21 +54,27 @@ type Options = * This provider loosely follows conventions that pre-date EIP-1193. * It is not compliant with any Ethereum provider standard. */ -export class InternalProvider { - readonly #rpcHandler: - | JsonRpcEngine - | JsonRpcServer>; +export class InternalProvider< + Middleware extends InternalProviderMiddleware = InternalProviderMiddleware, +> { + readonly #server: JsonRpcServer; /** * Construct a InternalProvider from a JSON-RPC server or legacy engine. * * @param options - Options. - * @param options.rpcHandler - The JSON-RPC server or engine used to process requests. Mutually exclusive with `engine`. - * @param options.engine - The JSON-RPC engine used to process requests. Mutually exclusive with `rpcHandler`. + * @param options.engine - **Deprecated:** The JSON-RPC engine used to process requests. Mutually exclusive with `server`. + * @param options.server - The JSON-RPC server used to process requests. Mutually exclusive with `engine`. */ - constructor(options: Options) { - this.#rpcHandler = - 'rpcHandler' in options ? options.rpcHandler : options.engine; + constructor(options: Options) { + const serverOrLegacyEngine = + 'server' in options ? options.server : options.engine; + this.#server = + 'push' in serverOrLegacyEngine + ? new JsonRpcServer({ + middleware: [asV2Middleware(serverOrLegacyEngine)], + }) + : serverOrLegacyEngine; } /** @@ -132,7 +147,7 @@ export class InternalProvider { ): Promise> => { // @ts-expect-error - The signatures are incompatible between the legacy engine // and server, but this works at runtime. - return await this.#rpcHandler.handle(jsonRpcRequest); + return await this.#server.handle(jsonRpcRequest); }; readonly #handleWithCallback = ( @@ -188,9 +203,5 @@ export function convertEip1193RequestToJsonRpcRequest< * @returns The deserialized error. */ function deserializeError(error: JsonRpcFailure['error']): JsonRpcError { - const jsonRpcError = new JsonRpcError(error.code, error.message, error.data); - if ('stack' in error) { - jsonRpcError.stack = error.stack; - } - return jsonRpcError; + return new JsonRpcError(error.code, error.message, error.data); } diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts index 22f5548122..18c93511bf 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts @@ -1,11 +1,10 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { asV2Middleware } from '@metamask/json-rpc-engine'; import type { JsonRpcMiddleware as LegacyJsonRpcMiddleware } from '@metamask/json-rpc-engine'; import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; -import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +import type { Json, JsonRpcParams } from '@metamask/utils'; +import type { InternalProviderMiddleware } from './internal-provider'; import { InternalProvider } from './internal-provider'; -import { providerFromEngine } from './provider-from-engine'; /** * Construct an Ethereum provider from the given middleware. @@ -18,10 +17,9 @@ export function providerFromMiddleware< Params extends JsonRpcParams, Result extends Json, >(middleware: LegacyJsonRpcMiddleware): InternalProvider { - const engine: JsonRpcEngine = new JsonRpcEngine(); - engine.push(middleware); - const provider: InternalProvider = providerFromEngine(engine); - return provider; + return providerFromMiddlewareV2( + asV2Middleware(middleware) as InternalProviderMiddleware, + ); } /** @@ -31,9 +29,9 @@ export function providerFromMiddleware< * @returns An Ethereum provider. */ export function providerFromMiddlewareV2< - Middleware extends JsonRpcMiddleware, + Middleware extends InternalProviderMiddleware, >(middleware: Middleware): InternalProvider { return new InternalProvider({ - rpcHandler: new JsonRpcServer({ middleware: [middleware] }), + server: new JsonRpcServer({ middleware: [middleware] }), }); } diff --git a/packages/json-rpc-engine/src/asV2Middleware.ts b/packages/json-rpc-engine/src/asV2Middleware.ts index 34f4bd3487..63dc09a83e 100644 --- a/packages/json-rpc-engine/src/asV2Middleware.ts +++ b/packages/json-rpc-engine/src/asV2Middleware.ts @@ -49,8 +49,9 @@ export function asV2Middleware< export function asV2Middleware< Params extends JsonRpcParams, Request extends JsonRpcRequest, + Result extends Json, >( - ...middleware: LegacyMiddleware[] + ...middleware: LegacyMiddleware[] ): JsonRpcMiddleware; /** @@ -69,7 +70,9 @@ export function asV2Middleware< ): JsonRpcMiddleware { const legacyMiddleware = typeof engineOrMiddleware === 'function' - ? mergeMiddleware([engineOrMiddleware, ...rest]) + ? // mergeMiddleware uses .asMiddleware() internally, which is necessary for our purposes. + // See comment on this below. + mergeMiddleware([engineOrMiddleware, ...rest]) : engineOrMiddleware.asMiddleware(); return async ({ request, context, next }) => { diff --git a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts index fe89e800ef..e1253cb6c0 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts @@ -91,11 +91,17 @@ type ContextOf = ? C : never; -export type MergedContextOf< - // Non-polluting `any` constraint. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Middleware extends JsonRpcMiddleware, -> = MergeContexts>; +// Non-polluting `any` constraint. +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type MiddlewareConstraint = JsonRpcMiddleware< + any, + ResultConstraint, + MiddlewareContext +>; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export type MergedContextOf = + MergeContexts>; const INVALID_ENGINE = Symbol('Invalid engine'); diff --git a/packages/json-rpc-engine/src/v2/JsonRpcServer.ts b/packages/json-rpc-engine/src/v2/JsonRpcServer.ts index bc0bbd0ed6..dbf782aec6 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcServer.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcServer.ts @@ -11,8 +11,8 @@ import { hasProperty, isObject } from '@metamask/utils'; import type { JsonRpcMiddleware, MergedContextOf, + MiddlewareConstraint, RequestOf, - ResultConstraint, } from './JsonRpcEngineV2'; import { JsonRpcEngineV2 } from './JsonRpcEngineV2'; import type { JsonRpcCall } from './utils'; @@ -20,7 +20,7 @@ import { getUniqueId } from '../getUniqueId'; type OnError = (error: unknown) => void; -type Options = { +type Options = { onError?: OnError; } & ( | { @@ -58,14 +58,7 @@ const jsonrpc = '2.0' as const; * ``` */ export class JsonRpcServer< - Middleware extends JsonRpcMiddleware< - // Non-polluting `any` constraint. - /* eslint-disable @typescript-eslint/no-explicit-any */ - any, - ResultConstraint, - any - /* eslint-enable @typescript-eslint/no-explicit-any */ - > = JsonRpcMiddleware, + Middleware extends MiddlewareConstraint = JsonRpcMiddleware, > { readonly #engine: JsonRpcEngineV2< RequestOf, From 84d3ae2b685f945aa1e87efe1a6919378c898cf5 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:17:20 -0700 Subject: [PATCH 06/19] docs: Update changelogs --- packages/eth-json-rpc-provider/CHANGELOG.md | 7 +++++-- packages/json-rpc-engine/CHANGELOG.md | 5 ++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index 90a38cb90f..051be24111 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -12,8 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Replace `SafeEventEmitterProvider` with `InternalProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - The new class is behaviorally equivalent to the previous version except it does not extend `SafeEventEmitter`. - `SafeEventEmitterProvider` is for now still exported as a deprecated alias of `InternalProvider` for backwards compatibility. -- Support constructing `InternalProvider` with a `JsonRpcServer` instance ([#7001](https://github.com/MetaMask/core/pull/7001)) - - The `rpcHandler` constructor option accepts either a `JsonRpcServer` or a legacy `JsonRpcEngine`. It is mutually exclusive with the `engine` option. +- **BREAKING:** Use `JsonRpcServer` instead of `JsonRpcEngine` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - Adds a new `server` constructor option to the `InternalProvider` class, mutually exclusive with the now deprecated `engine` option. + - Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcServer` internally + wherever they appear. Due to differences in error serialization, this may be + breaking for consumers. ## [5.0.1] diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index da4d5238af..b47e9d4981 100644 --- a/packages/json-rpc-engine/CHANGELOG.md +++ b/packages/json-rpc-engine/CHANGELOG.md @@ -9,9 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7032](https://github.com/MetaMask/core/pull/7032)) - - This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation. - See the readme for details. +- `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7001](https://github.com/MetaMask/core/pull/7001), [#7032](https://github.com/MetaMask/core/pull/7032)) + - This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation. See the readme for details. ## [10.1.1] From e41057a20ad28c3889e81c822bd82f8ea58619d0 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:44:06 -0800 Subject: [PATCH 07/19] fix: Handle errors properly in asV2Middleware --- packages/eth-json-rpc-provider/CHANGELOG.md | 4 +-- .../src/internal-provider.ts | 3 +- .../src/asV2Middleware.test.ts | 15 ++++++++++ .../json-rpc-engine/src/asV2Middleware.ts | 9 ++++-- .../src/v2/compatibility-utils.test.ts | 28 +++++++++---------- .../src/v2/compatibility-utils.ts | 2 +- 6 files changed, 40 insertions(+), 21 deletions(-) diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index 051be24111..b9f1ee1b1d 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -15,8 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Use `JsonRpcServer` instead of `JsonRpcEngine` ([#7001](https://github.com/MetaMask/core/pull/7001)) - Adds a new `server` constructor option to the `InternalProvider` class, mutually exclusive with the now deprecated `engine` option. - Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcServer` internally - wherever they appear. Due to differences in error serialization, this may be - breaking for consumers. + wherever they appear. Due to differences in error serialization, this may be + breaking for consumers. ## [5.0.1] diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index e5bba86b8b..cc0b7de50c 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -197,7 +197,8 @@ export function convertEip1193RequestToJsonRpcRequest< } /** - * Deserialize a JSON-RPC error. + * Deserialize a JSON-RPC error. Ignores the possibility of `stack` property, since this is + * stripped by `JsonRpcServer`. * * @param error - The JSON-RPC error to deserialize. * @returns The deserialized error. diff --git a/packages/json-rpc-engine/src/asV2Middleware.test.ts b/packages/json-rpc-engine/src/asV2Middleware.test.ts index d268b4cf3f..b91ef4c557 100644 --- a/packages/json-rpc-engine/src/asV2Middleware.test.ts +++ b/packages/json-rpc-engine/src/asV2Middleware.test.ts @@ -171,6 +171,21 @@ describe('asV2Middleware', () => { ); }); + it('does not forward undefined errors from legacy middleware', async () => { + const legacyMiddleware = jest.fn((_req, res, _next, end) => { + res.error = undefined; + res.result = 42; + end(); + }); + + const v2Engine = JsonRpcEngineV2.create({ + middleware: [asV2Middleware(legacyMiddleware)], + }); + + const result = await v2Engine.handle(makeRequest()); + expect(result).toBe(42); + }); + it('allows v2 engine to continue when legacy middleware does not end', async () => { const legacyMiddleware = jest.fn((_req, _res, next) => { next(); diff --git a/packages/json-rpc-engine/src/asV2Middleware.ts b/packages/json-rpc-engine/src/asV2Middleware.ts index 63dc09a83e..14333cbb4e 100644 --- a/packages/json-rpc-engine/src/asV2Middleware.ts +++ b/packages/json-rpc-engine/src/asV2Middleware.ts @@ -19,7 +19,7 @@ import { fromLegacyRequest, propagateToContext, propagateToRequest, - unserializeError, + deserializeError, } from './v2/compatibility-utils'; import type { // Used in docs. @@ -104,8 +104,11 @@ export function asV2Middleware< }); propagateToContext(req, context); - if (hasProperty(response, 'error')) { - throw unserializeError(response.error); + // Mimic the behavior of JsonRpcEngine.#handle(), which only treats truthy errors as errors. + // Legacy middleware may violate the invariant that response objects have either a result or an + // error property. In practice, we may see response objects with results and `{ error: undefined }`. + if (hasProperty(response, 'error') && response.error) { + throw deserializeError(response.error); } else if (hasProperty(response, 'result')) { return response.result as ResultConstraint; } diff --git a/packages/json-rpc-engine/src/v2/compatibility-utils.test.ts b/packages/json-rpc-engine/src/v2/compatibility-utils.test.ts index 9e75753daa..8e26b3ede9 100644 --- a/packages/json-rpc-engine/src/v2/compatibility-utils.test.ts +++ b/packages/json-rpc-engine/src/v2/compatibility-utils.test.ts @@ -7,7 +7,7 @@ import { makeContext, propagateToContext, propagateToRequest, - unserializeError, + deserializeError, } from './compatibility-utils'; import { MiddlewareContext } from './MiddlewareContext'; import { stringify } from './utils'; @@ -348,7 +348,7 @@ describe('compatibility-utils', () => { }); }); - describe('unserializeError', () => { + describe('deserializeError', () => { // Requires some special handling due to the possible existence or // non-existence of Error.isError describe('Error.isError', () => { @@ -382,7 +382,7 @@ describe('compatibility-utils', () => { isError.mockReturnValueOnce(true); const originalError = new Error('test error'); - const result = unserializeError(originalError); + const result = deserializeError(originalError); expect(result).toBe(originalError); }); @@ -390,14 +390,14 @@ describe('compatibility-utils', () => { isError.mockReturnValueOnce(false); const originalError = new Error('test error'); - const result = unserializeError(originalError); + const result = deserializeError(originalError); expect(result).toBe(originalError); }); }); it('creates a new Error when thrown value is a string', () => { const errorMessage = 'test error message'; - const result = unserializeError(errorMessage); + const result = deserializeError(errorMessage); expect(result).toBeInstanceOf(Error); expect(result.message).toBe(errorMessage); @@ -406,7 +406,7 @@ describe('compatibility-utils', () => { it.each([42, true, false, null, undefined, Symbol('test')])( 'creates a new Error with stringified message for non-object values', (value) => { - const result = unserializeError(value); + const result = deserializeError(value); expect(result).toBeInstanceOf(Error); expect(result.message).toBe(`Unknown error: ${stringify(value)}`); @@ -421,7 +421,7 @@ describe('compatibility-utils', () => { data: { foo: 'bar' }, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result).toBeInstanceOf(JsonRpcError); expect(result).toMatchObject({ @@ -439,7 +439,7 @@ describe('compatibility-utils', () => { data: { foo: 'bar' }, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result).toBeInstanceOf(Error); expect(result).not.toBeInstanceOf(JsonRpcError); @@ -455,7 +455,7 @@ describe('compatibility-utils', () => { message: 'test error message', }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result).toBeInstanceOf(Error); expect(result).not.toBeInstanceOf(JsonRpcError); @@ -469,7 +469,7 @@ describe('compatibility-utils', () => { stack: stackTrace, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result).toBeInstanceOf(Error); expect(result.stack).toBe(stackTrace); @@ -485,7 +485,7 @@ describe('compatibility-utils', () => { data, }; - const result = unserializeError(thrownValue) as JsonRpcError; + const result = deserializeError(thrownValue) as JsonRpcError; expect(result.cause).toBe(cause); expect(result.data).toStrictEqual({ @@ -499,7 +499,7 @@ describe('compatibility-utils', () => { code: 1234, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result.message).toBe('Unknown error'); }); @@ -510,7 +510,7 @@ describe('compatibility-utils', () => { message: 42, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result.message).toBe('Unknown error'); }); @@ -521,7 +521,7 @@ describe('compatibility-utils', () => { message: 42, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result.message).toBe('Internal JSON-RPC error.'); }); diff --git a/packages/json-rpc-engine/src/v2/compatibility-utils.ts b/packages/json-rpc-engine/src/v2/compatibility-utils.ts index 03257b6a2e..cdd65cb0de 100644 --- a/packages/json-rpc-engine/src/v2/compatibility-utils.ts +++ b/packages/json-rpc-engine/src/v2/compatibility-utils.ts @@ -135,7 +135,7 @@ export function propagateToRequest( * @param thrown - The thrown value to unserialize. * @returns The unserialized error. */ -export function unserializeError(thrown: unknown): Error | JsonRpcError { +export function deserializeError(thrown: unknown): Error | JsonRpcError { // @ts-expect-error - New, but preferred if available. // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError if (typeof Error.isError === 'function' && Error.isError(thrown)) { From 38b66a07a710a712c891339436a253e3e974d0df Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:44:58 -0800 Subject: [PATCH 08/19] test: Remove `undefined` from NetworkController "empty values" test cases --- packages/network-controller/src/create-network-client.ts | 2 +- .../tests/network-client/block-hash-in-response.ts | 2 +- .../tests/network-client/block-param.ts | 8 ++++---- .../tests/network-client/no-block-param.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 515c625816..687e7038b3 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -178,7 +178,7 @@ export function createNetworkClient({ const destroy = () => { // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/no-floating-promises + blockTracker.destroy(); }; diff --git a/packages/network-controller/tests/network-client/block-hash-in-response.ts b/packages/network-controller/tests/network-client/block-hash-in-response.ts index 0bfae7d9a7..a3f3e379de 100644 --- a/packages/network-controller/tests/network-client/block-hash-in-response.ts +++ b/packages/network-controller/tests/network-client/block-hash-in-response.ts @@ -203,7 +203,7 @@ export function testsForRpcMethodsThatCheckForBlockHashInResponse( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method }; const mockResult = emptyValue; diff --git a/packages/network-controller/tests/network-client/block-param.ts b/packages/network-controller/tests/network-client/block-param.ts index 6492ab9b38..f71326c6bd 100644 --- a/packages/network-controller/tests/network-client/block-param.ts +++ b/packages/network-controller/tests/network-client/block-param.ts @@ -209,7 +209,7 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method, @@ -1227,7 +1227,7 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method, @@ -1360,7 +1360,7 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { if (providerType === 'infura') { it(`retries up to 10 times if a "${emptyValue}" response is returned, returning successful non-empty response if there is one on the 10th try`, async () => { const request = { @@ -1555,7 +1555,7 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method, diff --git a/packages/network-controller/tests/network-client/no-block-param.ts b/packages/network-controller/tests/network-client/no-block-param.ts index 97b6cbd10e..d79f4357f0 100644 --- a/packages/network-controller/tests/network-client/no-block-param.ts +++ b/packages/network-controller/tests/network-client/no-block-param.ts @@ -149,7 +149,7 @@ export function testsForRpcMethodAssumingNoBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method }; const mockResult = emptyValue; From 3c58ca350e70187d0fb7bde813ab49a7d937b4a4 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:37:45 -0800 Subject: [PATCH 09/19] chore: Restore eslint directive --- packages/network-controller/src/create-network-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 687e7038b3..515c625816 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -178,7 +178,7 @@ export function createNetworkClient({ const destroy = () => { // TODO: Either fix this lint violation or explain why it's necessary to ignore. - + // eslint-disable-next-line @typescript-eslint/no-floating-promises blockTracker.destroy(); }; From fa7e3e846f371730b5732bb178d82e3289653842 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:15:33 -0800 Subject: [PATCH 10/19] refactor: Touch up internal types, docs --- packages/eth-json-rpc-provider/src/internal-provider.ts | 8 +++++--- .../eth-json-rpc-provider/src/provider-from-middleware.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index cc0b7de50c..a3d9950a23 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -145,9 +145,11 @@ export class InternalProvider< readonly #handle = async ( jsonRpcRequest: JsonRpcRequest, ): Promise> => { - // @ts-expect-error - The signatures are incompatible between the legacy engine - // and server, but this works at runtime. - return await this.#server.handle(jsonRpcRequest); + // This typecast is technicaly unsafe, but we need it to preserve the provider's + // public interface, which allows you to typecast results. + return (await this.#server.handle( + jsonRpcRequest, + )) as JsonRpcResponse; }; readonly #handleWithCallback = ( diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts index 18c93511bf..ec4b748d8d 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts @@ -11,7 +11,7 @@ import { InternalProvider } from './internal-provider'; * * @param middleware - The middleware to construct a provider from. * @returns An Ethereum provider. - * @deprecated Use {@link providerFromMiddlewareV2} instead. + * @deprecated Use `JsonRpcEngineV2` middleware and {@link providerFromMiddlewareV2} instead. */ export function providerFromMiddleware< Params extends JsonRpcParams, From 3156a14bbd9f9ad066bc85224c719110dfb42d2f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:21:08 -0800 Subject: [PATCH 11/19] refactor!: Remove providerFromEngine --- .../tests/withBlockTracker.ts | 8 +--- .../test/util/helpers.ts | 7 +-- packages/eth-json-rpc-provider/CHANGELOG.md | 10 ++-- .../eth-json-rpc-provider/src/index.test.ts | 1 - packages/eth-json-rpc-provider/src/index.ts | 1 - .../src/provider-from-engine.test.ts | 46 ------------------- .../src/provider-from-engine.ts | 14 ------ .../src/create-network-client.ts | 5 +- 8 files changed, 13 insertions(+), 79 deletions(-) delete mode 100644 packages/eth-json-rpc-provider/src/provider-from-engine.test.ts delete mode 100644 packages/eth-json-rpc-provider/src/provider-from-engine.ts diff --git a/packages/eth-block-tracker/tests/withBlockTracker.ts b/packages/eth-block-tracker/tests/withBlockTracker.ts index 39c227da90..ca866459f5 100644 --- a/packages/eth-block-tracker/tests/withBlockTracker.ts +++ b/packages/eth-block-tracker/tests/withBlockTracker.ts @@ -1,8 +1,4 @@ -import { providerFromEngine } from '@metamask/eth-json-rpc-provider'; -import type { - // Eip1193Request, - InternalProvider, -} from '@metamask/eth-json-rpc-provider'; +import { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { Json } from '@metamask/utils'; import util from 'util'; @@ -98,7 +94,7 @@ function getFakeProvider({ }); } - const provider = providerFromEngine(new JsonRpcEngine()); + const provider = new InternalProvider({ engine: new JsonRpcEngine() }); jest .spyOn(provider, 'request') .mockImplementation(async (eip1193Request): Promise => { diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index dd4b6ce2d2..6530868827 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -1,8 +1,5 @@ import { PollingBlockTracker } from '@metamask/eth-block-tracker'; -import { - providerFromEngine, - type InternalProvider, -} from '@metamask/eth-json-rpc-provider'; +import { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { JsonRpcEngine, type JsonRpcMiddleware, @@ -96,7 +93,7 @@ export function createFinalMiddlewareWithDefaultResult< */ export function createProviderAndBlockTracker() { const engine = new JsonRpcEngine(); - const provider = providerFromEngine(engine); + const provider = new InternalProvider({ engine }); const blockTracker = new PollingBlockTracker({ provider, diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index b9f1ee1b1d..302c118ed0 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -14,9 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `SafeEventEmitterProvider` is for now still exported as a deprecated alias of `InternalProvider` for backwards compatibility. - **BREAKING:** Use `JsonRpcServer` instead of `JsonRpcEngine` ([#7001](https://github.com/MetaMask/core/pull/7001)) - Adds a new `server` constructor option to the `InternalProvider` class, mutually exclusive with the now deprecated `engine` option. - - Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcServer` internally - wherever they appear. Due to differences in error serialization, this may be - breaking for consumers. + - Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcServer` internally wherever they appear. + This change should mostly be unobservable. However, due to differences in error handling, this may be breaking for consumers. + +### Removed + +- **BREAKING:** Remove `providerFromEngine` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - Use `InternalProvider` directly instead. ## [5.0.1] diff --git a/packages/eth-json-rpc-provider/src/index.test.ts b/packages/eth-json-rpc-provider/src/index.test.ts index eb6630252e..1fa713b79b 100644 --- a/packages/eth-json-rpc-provider/src/index.test.ts +++ b/packages/eth-json-rpc-provider/src/index.test.ts @@ -6,7 +6,6 @@ describe('Package exports', () => { Array [ "InternalProvider", "SafeEventEmitterProvider", - "providerFromEngine", "providerFromMiddleware", "providerFromMiddlewareV2", ] diff --git a/packages/eth-json-rpc-provider/src/index.ts b/packages/eth-json-rpc-provider/src/index.ts index a16f98b675..a4bab7a5db 100644 --- a/packages/eth-json-rpc-provider/src/index.ts +++ b/packages/eth-json-rpc-provider/src/index.ts @@ -1,6 +1,5 @@ import { InternalProvider } from './internal-provider'; -export * from './provider-from-engine'; export * from './provider-from-middleware'; /** diff --git a/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts b/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts deleted file mode 100644 index 6e7589aa7b..0000000000 --- a/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import { providerErrors } from '@metamask/rpc-errors'; - -import { providerFromEngine } from './provider-from-engine'; - -describe('providerFromEngine', () => { - it('handles a successful request', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, res, _next, end) => { - res.result = 42; - end(); - }); - const provider = providerFromEngine(engine); - const exampleRequest = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - const response = await provider.request(exampleRequest); - - expect(response).toBe(42); - }); - - it('handles a failed request', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, _res, _next, end) => { - end( - providerErrors.custom({ - code: 1001, - message: 'Test error', - }), - ); - }); - const provider = providerFromEngine(engine); - const exampleRequest = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => provider.request(exampleRequest)).rejects.toThrow( - 'Test error', - ); - }); -}); diff --git a/packages/eth-json-rpc-provider/src/provider-from-engine.ts b/packages/eth-json-rpc-provider/src/provider-from-engine.ts deleted file mode 100644 index d8800def44..0000000000 --- a/packages/eth-json-rpc-provider/src/provider-from-engine.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { JsonRpcEngine } from '@metamask/json-rpc-engine'; - -import { InternalProvider } from './internal-provider'; - -/** - * Construct an Ethereum provider from a JSON-RPC engine. - * - * @param engine - The JSON-RPC engine to construct a provider from. - * @returns An Ethereum provider. - * @deprecated Just use {@link InternalProvider} directly instead. - */ -export function providerFromEngine(engine: JsonRpcEngine): InternalProvider { - return new InternalProvider({ engine }); -} diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 515c625816..c4cb46ec4d 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -12,9 +12,8 @@ import { createFetchMiddleware, createRetryOnEmptyMiddleware, } from '@metamask/eth-json-rpc-middleware'; -import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { - providerFromEngine, + InternalProvider, providerFromMiddleware, } from '@metamask/eth-json-rpc-provider'; import { @@ -174,7 +173,7 @@ export function createNetworkClient({ engine.push(networkMiddleware); - const provider = providerFromEngine(engine); + const provider = new InternalProvider({ engine }); const destroy = () => { // TODO: Either fix this lint violation or explain why it's necessary to ignore. From ea28c55d8b20b37e8662fec070d6a18057bd7e09 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:07:54 -0800 Subject: [PATCH 12/19] refactor: Remove undefined from empty values array --- packages/eth-json-rpc-middleware/src/retryOnEmpty.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts b/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts index 2b454ff4fe..f9447a8dbc 100644 --- a/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts +++ b/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts @@ -21,11 +21,7 @@ import { timeout } from './utils/timeout'; const log = createModuleLogger(projectLogger, 'retry-on-empty'); // empty values used to determine if a request should be retried // `` comes from https://github.com/ethereum/go-ethereum/issues/16925 -const emptyValues: (string | null | undefined)[] = [ - undefined, - null, - '\u003cnil\u003e', -]; +const emptyValues = [null, '\u003cnil\u003e']; /** * Creates a middleware that retries requests with empty responses. From 4c0a2f474366b574bf044d7a5fc84bb64db486f2 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:08:09 -0800 Subject: [PATCH 13/19] docs: Update changelogs --- packages/eth-json-rpc-middleware/CHANGELOG.md | 4 ++++ packages/network-controller/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index d8f6c686f1..673180d189 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Use `InternalProvider` instead of `SafeEventEmitterProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - Wherever a `SafeEventEmitterProvider` was expected, an `InternalProvider` is now expected instead. +- **BREAKING:** Stop retrying `undefined` results for methods that include a block tag parameter ([#7001](https://github.com/MetaMask/core/pull/7001)) + - The `retryOnEmpty` middleware will now throw an error if it encounters an `undefined` result when dispatching + a request with a later block number than the originally requested block number. + - In practice, this should happen rarely if ever. - Migrate all uses of `interface` to `type` ([#6885](https://github.com/MetaMask/core/pull/6885)) ## [21.0.0] diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 879bc61d25..8e2ec5d161 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Use `InternalProvider` instead of `SafeEventEmitterProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - Providers accessible either via network clients or global proxies no longer emit events (or inherit from EventEmitter, for that matter). +- **BREAKING:** Stop retrying `undefined` results for methods that include a block tag parameter ([#7001](https://github.com/MetaMask/core/pull/7001)) + - The network client middleware, via `@metamask/eth-json-rpc-middleware`, will now throw an error if it encounters an + `undefined` result when dispatching a request with a later block number than the originally requested block number. + - In practice, this should happen rarely if ever. - Bump `@metamask/controller-utils` from `^11.14.1` to `^11.15.0` ([#7003](https://github.com/MetaMask/core/pull/7003)) ### Fixed From 4685f4a1a5ac1a5213481c9470dced40371ab833 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:13:57 -0800 Subject: [PATCH 14/19] refactor: Use JsonRpcEngineV2 instead of JsonRpcServer --- packages/eth-json-rpc-provider/CHANGELOG.md | 5 +- packages/eth-json-rpc-provider/package.json | 3 +- .../src/internal-provider.test.ts | 701 ++++++++---------- .../src/internal-provider.ts | 140 ++-- .../src/provider-from-middleware.ts | 25 +- .../json-rpc-engine/src/v2/JsonRpcEngineV2.ts | 9 + .../src/v2/MiddlewareContext.ts | 3 + packages/json-rpc-engine/src/v2/index.ts | 5 +- 8 files changed, 420 insertions(+), 471 deletions(-) diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index 302c118ed0..39ef973fcf 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -12,9 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Replace `SafeEventEmitterProvider` with `InternalProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - The new class is behaviorally equivalent to the previous version except it does not extend `SafeEventEmitter`. - `SafeEventEmitterProvider` is for now still exported as a deprecated alias of `InternalProvider` for backwards compatibility. -- **BREAKING:** Use `JsonRpcServer` instead of `JsonRpcEngine` ([#7001](https://github.com/MetaMask/core/pull/7001)) - - Adds a new `server` constructor option to the `InternalProvider` class, mutually exclusive with the now deprecated `engine` option. - - Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcServer` internally wherever they appear. +- **BREAKING:** Migrate from `JsonRpcEngine` to `JsonRpcEngineV2` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcEngineV2` internally wherever they appear. This change should mostly be unobservable. However, due to differences in error handling, this may be breaking for consumers. ### Removed diff --git a/packages/eth-json-rpc-provider/package.json b/packages/eth-json-rpc-provider/package.json index 1796e2e443..e7daf95511 100644 --- a/packages/eth-json-rpc-provider/package.json +++ b/packages/eth-json-rpc-provider/package.json @@ -55,8 +55,7 @@ "dependencies": { "@metamask/json-rpc-engine": "^10.1.1", "@metamask/rpc-errors": "^7.0.2", - "@metamask/utils": "^11.8.1", - "uuid": "^8.3.2" + "@metamask/utils": "^11.8.1" }, "devDependencies": { "@ethersproject/providers": "^5.7.0", diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index 979a0825ea..ea061cb5e3 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -3,13 +3,11 @@ import EthQuery from '@metamask/eth-query'; import EthJsQuery from '@metamask/ethjs-query'; import { asV2Middleware, JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; -import { JsonRpcEngineV2, JsonRpcServer } from '@metamask/json-rpc-engine/v2'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { type JsonRpcRequest, type Json } from '@metamask/utils'; import { BrowserProvider } from 'ethers'; import { promisify } from 'util'; -// eslint-disable-next-line import-x/namespace -import * as uuid from 'uuid'; import { InternalProvider, @@ -20,7 +18,7 @@ jest.mock('uuid'); type ResultParam = Json | ((req?: JsonRpcRequest) => Json); -const createEngine = (method: string, result: ResultParam) => { +const createLegacyEngine = (method: string, result: ResultParam) => { const engine = new JsonRpcEngine(); engine.push((req, res, next, end) => { if (req.method === method) { @@ -32,438 +30,394 @@ const createEngine = (method: string, result: ResultParam) => { return engine; }; -const createServer = (method: string, result: ResultParam) => { - const engine = JsonRpcEngineV2.create>({ +const createV2Engine = (method: string, result: ResultParam) => { + return JsonRpcEngineV2.create>({ middleware: [ ({ request, next }) => { if (request.method === method) { - return typeof result === 'function' - ? result(request as JsonRpcRequest) - : result; + return typeof result === 'function' ? result(request) : result; } return next(); }, ], }); - return new JsonRpcServer>({ engine }); }; describe('legacy constructor', () => { it('can be constructed with an engine', () => { const provider = new InternalProvider({ - engine: createEngine('eth_blockNumber', 42), + engine: createLegacyEngine('eth_blockNumber', 42), }); expect(provider).toBeDefined(); }); }); -const createOptions = ( - paramName: 'engine' | 'server', - rpcHandler: ReturnType, -) => - ({ - [paramName]: rpcHandler, - }) as ConstructorParameters[0]; - describe.each([ { - createRpcHandler: createEngine, + createRpcHandler: createLegacyEngine, name: 'JsonRpcEngine', - paramName: 'engine', }, { - createRpcHandler: createServer, + createRpcHandler: createV2Engine, name: 'JsonRpcServer', - paramName: 'server', }, -] as const)( - 'InternalProvider with $name', - ({ createRpcHandler, paramName }) => { - it('returns the correct block number with @metamask/eth-query', async () => { - const provider = new InternalProvider( - createOptions(paramName, createRpcHandler('eth_blockNumber', 42)), - ); - const ethQuery = new EthQuery(provider); +] as const)('InternalProvider with $name', ({ createRpcHandler }) => { + it('returns the correct block number with @metamask/eth-query', async () => { + const provider = new InternalProvider({ + engine: createRpcHandler('eth_blockNumber', 42), + }); + const ethQuery = new EthQuery(provider); - ethQuery.sendAsync({ method: 'eth_blockNumber' }, (_error, response) => { - expect(response).toBe(42); - }); + ethQuery.sendAsync({ method: 'eth_blockNumber' }, (_error, response) => { + expect(response).toBe(42); }); + }); - it('returns the correct block number with @metamask/ethjs-query', async () => { - const provider = new InternalProvider( - createOptions(paramName, createRpcHandler('eth_blockNumber', 42)), - ); - const ethJsQuery = new EthJsQuery(provider); + it('returns the correct block number with @metamask/ethjs-query', async () => { + const provider = new InternalProvider({ + engine: createRpcHandler('eth_blockNumber', 42), + }); + const ethJsQuery = new EthJsQuery(provider); - const response = await ethJsQuery.blockNumber(); + const response = await ethJsQuery.blockNumber(); + + expect(response.toNumber()).toBe(42); + }); - expect(response.toNumber()).toBe(42); + it('returns the correct block number with Web3Provider', async () => { + const provider = new InternalProvider({ + engine: createRpcHandler('eth_blockNumber', 42), }); + const web3Provider = new Web3Provider(provider); - it('returns the correct block number with Web3Provider', async () => { - const provider = new InternalProvider( - createOptions(paramName, createRpcHandler('eth_blockNumber', 42)), - ); - const web3Provider = new Web3Provider(provider); + const response = await web3Provider.send('eth_blockNumber', []); - const response = await web3Provider.send('eth_blockNumber', []); + expect(response).toBe(42); + }); - expect(response).toBe(42); + it('returns the correct block number with BrowserProvider', async () => { + const provider = new InternalProvider({ + engine: createRpcHandler('eth_blockNumber', 42), }); + const browserProvider = new BrowserProvider(provider); - it('returns the correct block number with BrowserProvider', async () => { - const provider = new InternalProvider( - createOptions(paramName, createRpcHandler('eth_blockNumber', 42)), - ); - const browserProvider = new BrowserProvider(provider); + const response = await browserProvider.send('eth_blockNumber', []); - const response = await browserProvider.send('eth_blockNumber', []); + expect(response).toBe(42); - expect(response).toBe(42); + browserProvider.destroy(); + }); - browserProvider.destroy(); + describe('request', () => { + it('handles a successful JSON-RPC object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; + }); + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + + const result = await provider.request(request); + + expect(req).toStrictEqual({ + id: expect.any(Number), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(result).toBe(42); }); - describe('request', () => { - it('handles a successful JSON-RPC object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - - const result = await provider.request(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }); - expect(result).toBe(42); + it('handles a successful EIP-1193 object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - - it('handles a successful EIP-1193 object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const request = { - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - - const result = await provider.request(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }); - expect(result).toBe(42); + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + + const result = await provider.request(request); + + expect(req).toStrictEqual({ + id: expect.any(Number), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, }); + expect(result).toBe(42); + }); - it('handles a failure with a non-JSON-RPC error', async () => { - const rpcHandler = createRpcHandler('test', () => { - throw providerErrors.custom({ - code: 1001, - message: 'Test error', - data: { cause: 'Test cause' }, - }); + it('handles a failure with a non-JSON-RPC error', async () => { + const rpcHandler = createRpcHandler('test', () => { + throw providerErrors.custom({ + code: 1001, + message: 'Test error', + data: { cause: 'Test cause' }, }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => provider.request(request)).rejects.toThrow( - providerErrors.custom({ - code: 1001, - message: 'Test error', - data: { cause: 'Test cause' }, - }), - ); }); + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => provider.request(request)).rejects.toThrow( + providerErrors.custom({ + code: 1001, + message: 'Test error', + data: { cause: 'Test cause' }, + }), + ); + }); - it('handles a failure with a JSON-RPC error', async () => { - const rpcHandler = createRpcHandler('test', () => { - throw new Error('Test error'); - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => provider.request(request)).rejects.toThrow( - rpcErrors.internal({ - message: 'Test error', - data: { cause: 'Test cause' }, - }), - ); + it('handles a failure with a JSON-RPC error', async () => { + const rpcHandler = createRpcHandler('test', () => { + throw new Error('Test error'); }); + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => provider.request(request)).rejects.toThrow( + rpcErrors.internal({ + message: 'Test error', + data: { cause: 'Test cause' }, + }), + ); }); + }); - describe('sendAsync', () => { - it('handles a successful JSON-RPC object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const promisifiedSendAsync = promisify(provider.sendAsync); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - - const response = await promisifiedSendAsync(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }); - expect(response.result).toBe(42); + describe('sendAsync', () => { + it('handles a successful JSON-RPC object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - - it('handles a successful EIP-1193 object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const promisifiedSendAsync = promisify(provider.sendAsync); - const request = { - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - - const response = await promisifiedSendAsync(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }); - expect(response.result).toBe(42); + const provider = new InternalProvider({ engine: rpcHandler }); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + + const response = await promisifiedSendAsync(request); + + expect(req).toStrictEqual({ + id: expect.any(Number), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, }); + expect(response.result).toBe(42); + }); - it('handles a failed request', async () => { - const rpcHandler = createRpcHandler('test', () => { - throw new Error('Test error'); - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const promisifiedSendAsync = promisify(provider.sendAsync); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( - 'Test error', - ); + it('handles a successful EIP-1193 object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - - it('handles an error thrown by the JSON-RPC handler', async () => { - let rpcHandler = createRpcHandler('test', () => null); - // Transform the engine into a server so we can mock the "handle" method. - // The "handle" method should never throw, but we should be resilient to it anyway. - rpcHandler = - // eslint-disable-next-line jest/no-conditional-in-test - 'push' in rpcHandler - ? new JsonRpcServer({ middleware: [asV2Middleware(rpcHandler)] }) - : rpcHandler; - jest - .spyOn(rpcHandler, 'handle') - .mockRejectedValue(new Error('Test error')); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const promisifiedSendAsync = promisify(provider.sendAsync); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( - 'Test error', - ); + const provider = new InternalProvider({ engine: rpcHandler }); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + + const response = await promisifiedSendAsync(request); + + expect(req).toStrictEqual({ + id: expect.any(Number), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, }); + expect(response.result).toBe(42); }); - describe('send', () => { - it('throws if a callback is not provided', () => { - const rpcHandler = createRpcHandler('test', 42); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - // @ts-expect-error - Destructive testing. - expect(() => provider.send(request)).toThrow( - 'Must provide callback to "send" method.', - ); + it('handles a failed request', async () => { + const rpcHandler = createRpcHandler('test', () => { + throw new Error('Test error'); }); + const provider = new InternalProvider({ engine: rpcHandler }); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( + 'Test error', + ); + }); - it('handles a successful JSON-RPC object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const promisifiedSend = promisify(provider.send); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - - const response = await promisifiedSend(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }); - expect(response.result).toBe(42); + it('handles an error thrown by the JSON-RPC handler', async () => { + let rpcHandler = createRpcHandler('test', () => null); + // Transform the engine into a server so we can mock the "handle" method. + // The "handle" method should never throw, but we should be resilient to it anyway. + rpcHandler = + // eslint-disable-next-line jest/no-conditional-in-test + 'push' in rpcHandler + ? JsonRpcEngineV2.create({ middleware: [asV2Middleware(rpcHandler)] }) + : rpcHandler; + jest + .spyOn(rpcHandler, 'handle') + .mockRejectedValue(new Error('Test error')); + const provider = new InternalProvider({ engine: rpcHandler }); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( + 'Test error', + ); + }); + }); + + describe('send', () => { + it('throws if a callback is not provided', () => { + const rpcHandler = createRpcHandler('test', 42); + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + // @ts-expect-error - Destructive testing. + expect(() => provider.send(request)).toThrow( + 'Must provide callback to "send" method.', + ); + }); + + it('handles a successful JSON-RPC object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); + const provider = new InternalProvider({ engine: rpcHandler }); + const promisifiedSend = promisify(provider.send); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + + const response = await promisifiedSend(request); + + expect(req).toStrictEqual({ + id: expect.any(Number), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(response.result).toBe(42); + }); - it('handles a successful EIP-1193 object request', async () => { - let req: JsonRpcRequest | undefined; - const rpcHandler = createRpcHandler('test', (request) => { - req = request; - return 42; - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const promisifiedSend = promisify(provider.send); - const request = { - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - - const response = await promisifiedSend(request); - - expect(req).toStrictEqual({ - id: expect.anything(), - jsonrpc: '2.0' as const, - method: 'test', - params: { - param1: 'value1', - param2: 'value2', - }, - }); - expect(response.result).toBe(42); + it('handles a successful EIP-1193 object request', async () => { + let req: JsonRpcRequest | undefined; + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); + const provider = new InternalProvider({ engine: rpcHandler }); + const promisifiedSend = promisify(provider.send); + const request = { + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }; + + const response = await promisifiedSend(request); + + expect(req).toStrictEqual({ + id: expect.any(Number), + jsonrpc: '2.0' as const, + method: 'test', + params: { + param1: 'value1', + param2: 'value2', + }, + }); + expect(response.result).toBe(42); + }); - it('handles a failed request', async () => { - const rpcHandler = createRpcHandler('test', () => { - throw new Error('Test error'); - }); - const provider = new InternalProvider( - createOptions(paramName, rpcHandler), - ); - const promisifiedSend = promisify(provider.send); - const request = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => promisifiedSend(request)).rejects.toThrow( - 'Test error', - ); + it('handles a failed request', async () => { + const rpcHandler = createRpcHandler('test', () => { + throw new Error('Test error'); }); + const provider = new InternalProvider({ engine: rpcHandler }); + const promisifiedSend = promisify(provider.send); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => promisifiedSend(request)).rejects.toThrow( + 'Test error', + ); }); - }, -); + }); +}); describe('convertEip1193RequestToJsonRpcRequest', () => { it('generates a unique id if id is not provided', () => { - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -473,7 +427,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: 'mock-id', + id: expect.any(Number), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -481,7 +435,6 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { }); it('uses the default jsonrpc version if not provided', () => { - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -491,7 +444,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: 'mock-id', + id: expect.any(Number), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -499,7 +452,6 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { }); it('uses the provided jsonrpc version if provided', () => { - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { jsonrpc: '2.0' as const, method: 'test', @@ -510,7 +462,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: 'mock-id', + id: expect.any(Number), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -518,7 +470,6 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { }); it('uses an empty object as params if not provided', () => { - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { method: 'test', }; @@ -527,7 +478,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: 'mock-id', + id: expect.any(Number), jsonrpc: '2.0', method: 'test', }); diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index a3d9950a23..d8431e15be 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -1,52 +1,38 @@ -import { asV2Middleware, type JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; -import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; -import { JsonRpcError } from '@metamask/rpc-errors'; -import type { JsonRpcFailure } from '@metamask/utils'; import { - hasProperty, + asV2Middleware, + getUniqueId, + type JsonRpcEngine, +} from '@metamask/json-rpc-engine'; +import type { + ContextConstraint, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; +import type { JsonRpcSuccess } from '@metamask/utils'; +import { type Json, type JsonRpcId, type JsonRpcParams, type JsonRpcRequest, - type JsonRpcResponse, type JsonRpcVersion2, } from '@metamask/utils'; -import { v4 as uuidV4 } from 'uuid'; /** * A JSON-RPC request conforming to the EIP-1193 specification. */ -type Eip1193Request = { +type Eip1193Request = { id?: JsonRpcId; jsonrpc?: JsonRpcVersion2; method: string; params?: Params; }; -/** - * The {@link JsonRpcMiddleware} constraint and default type for the {@link InternalProvider}. - * We care that the middleware can handle JSON-RPC requests, but do not care about the context, - * the validity of which is enforced by the {@link JsonRpcServer}. - */ -export type InternalProviderMiddleware = JsonRpcMiddleware< - JsonRpcRequest, - Json, - // Non-polluting `any` constraint. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any ->; - -type Options = - | { - /** - * @deprecated Use `server` instead. - */ - engine: JsonRpcEngine; - } - | { - server: JsonRpcServer; - }; +type Options< + Request extends JsonRpcRequest = JsonRpcRequest, + Context extends ContextConstraint = MiddlewareContext, +> = { + engine: JsonRpcEngine | JsonRpcEngineV2; +}; /** * An Ethereum provider. @@ -54,27 +40,22 @@ type Options = * This provider loosely follows conventions that pre-date EIP-1193. * It is not compliant with any Ethereum provider standard. */ -export class InternalProvider< - Middleware extends InternalProviderMiddleware = InternalProviderMiddleware, -> { - readonly #server: JsonRpcServer; +export class InternalProvider { + readonly #engine: JsonRpcEngineV2; /** * Construct a InternalProvider from a JSON-RPC server or legacy engine. * * @param options - Options. - * @param options.engine - **Deprecated:** The JSON-RPC engine used to process requests. Mutually exclusive with `server`. - * @param options.server - The JSON-RPC server used to process requests. Mutually exclusive with `engine`. + * @param options.engine - The JSON-RPC engine used to process requests. */ - constructor(options: Options) { - const serverOrLegacyEngine = - 'server' in options ? options.server : options.engine; - this.#server = - 'push' in serverOrLegacyEngine - ? new JsonRpcServer({ - middleware: [asV2Middleware(serverOrLegacyEngine)], + constructor({ engine }: Options) { + this.#engine = + 'push' in engine + ? JsonRpcEngineV2.create({ + middleware: [asV2Middleware(engine)], }) - : serverOrLegacyEngine; + : engine; } /** @@ -88,13 +69,7 @@ export class InternalProvider< ): Promise { const jsonRpcRequest = convertEip1193RequestToJsonRpcRequest(eip1193Request); - const response: JsonRpcResponse = - await this.#handle(jsonRpcRequest); - - if ('result' in response) { - return response.result; - } - throw deserializeError(response.error); + return (await this.#handle(jsonRpcRequest)).result; } /** @@ -105,7 +80,8 @@ export class InternalProvider< * * @param eip1193Request - The request to send. * @param callback - A function that is called upon the success or failure of the request. - * @deprecated Use {@link request} instead. + * @deprecated Use {@link request} instead. This method is retained solely for backwards + * compatibility with certain libraries. */ sendAsync = ( eip1193Request: Eip1193Request, @@ -126,7 +102,8 @@ export class InternalProvider< * * @param eip1193Request - The request to send. * @param callback - A function that is called upon the success or failure of the request. - * @deprecated Use {@link request} instead. + * @deprecated Use {@link request} instead. This method is retained solely for backwards + * compatibility with certain libraries. */ send = ( eip1193Request: Eip1193Request, @@ -144,31 +121,33 @@ export class InternalProvider< readonly #handle = async ( jsonRpcRequest: JsonRpcRequest, - ): Promise> => { + ): Promise> => { + const { id, jsonrpc } = jsonRpcRequest; // This typecast is technicaly unsafe, but we need it to preserve the provider's // public interface, which allows you to typecast results. - return (await this.#server.handle( + const result = (await this.#engine.handle( jsonRpcRequest, - )) as JsonRpcResponse; + )) as unknown as Result; + + return { + id, + jsonrpc, + result, + }; }; readonly #handleWithCallback = ( jsonRpcRequest: JsonRpcRequest, callback: (error: unknown, providerRes?: unknown) => void, ): void => { - /* eslint-disable promise/always-return,promise/no-callback-in-promise */ + /* eslint-disable promise/no-callback-in-promise */ this.#handle(jsonRpcRequest) - .then((response) => { - if (hasProperty(response, 'result')) { - callback(null, response); - } else { - callback(deserializeError(response.error)); - } - }) + // A resolution will always be a successful response + .then((response) => callback(null, response)) .catch((error) => { callback(error); }); - /* eslint-enable promise/always-return,promise/no-callback-in-promise */ + /* eslint-enable promise/no-callback-in-promise */ }; } @@ -178,12 +157,16 @@ export class InternalProvider< * @param eip1193Request - The EIP-1193 request to convert. * @returns The JSON-RPC request. */ -export function convertEip1193RequestToJsonRpcRequest< - Params extends JsonRpcParams, ->( - eip1193Request: Eip1193Request, -): JsonRpcRequest> { - const { id = uuidV4(), jsonrpc = '2.0', method, params } = eip1193Request; +export function convertEip1193RequestToJsonRpcRequest( + eip1193Request: Eip1193Request, +): JsonRpcRequest { + const { + id = getUniqueId(), + jsonrpc = '2.0', + method, + params, + } = eip1193Request; + return params ? { id, @@ -197,14 +180,3 @@ export function convertEip1193RequestToJsonRpcRequest< method, }; } - -/** - * Deserialize a JSON-RPC error. Ignores the possibility of `stack` property, since this is - * stripped by `JsonRpcServer`. - * - * @param error - The JSON-RPC error to deserialize. - * @returns The deserialized error. - */ -function deserializeError(error: JsonRpcFailure['error']): JsonRpcError { - return new JsonRpcError(error.code, error.message, error.data); -} diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts index ec4b748d8d..33905a62c8 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts @@ -1,9 +1,15 @@ import { asV2Middleware } from '@metamask/json-rpc-engine'; import type { JsonRpcMiddleware as LegacyJsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; -import type { Json, JsonRpcParams } from '@metamask/utils'; +import type { + JsonRpcMiddleware, + ResultConstraint, +} from '@metamask/json-rpc-engine/v2'; +import { + JsonRpcEngineV2, + type ContextConstraint, +} from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; -import type { InternalProviderMiddleware } from './internal-provider'; import { InternalProvider } from './internal-provider'; /** @@ -18,7 +24,7 @@ export function providerFromMiddleware< Result extends Json, >(middleware: LegacyJsonRpcMiddleware): InternalProvider { return providerFromMiddlewareV2( - asV2Middleware(middleware) as InternalProviderMiddleware, + asV2Middleware(middleware) as JsonRpcMiddleware, ); } @@ -29,9 +35,16 @@ export function providerFromMiddleware< * @returns An Ethereum provider. */ export function providerFromMiddlewareV2< - Middleware extends InternalProviderMiddleware, + Request extends JsonRpcRequest, + Middleware extends JsonRpcMiddleware< + Request, + ResultConstraint, + ContextConstraint + >, >(middleware: Middleware): InternalProvider { return new InternalProvider({ - server: new JsonRpcServer({ middleware: [middleware] }), + engine: JsonRpcEngineV2.create({ + middleware: [middleware as JsonRpcMiddleware], + }), }); } diff --git a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts index e1253cb6c0..a6c4dce794 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts @@ -73,6 +73,9 @@ type ConstructorOptions< >; }; +/** + * The request type of a middleware. + */ export type RequestOf = Middleware extends JsonRpcMiddleware< infer Request, @@ -91,6 +94,9 @@ type ContextOf = ? C : never; +/** + * A constraint for {@link JsonRpcMiddleware} generic parameters. + */ // Non-polluting `any` constraint. /* eslint-disable @typescript-eslint/no-explicit-any */ export type MiddlewareConstraint = JsonRpcMiddleware< @@ -100,6 +106,9 @@ export type MiddlewareConstraint = JsonRpcMiddleware< >; /* eslint-enable @typescript-eslint/no-explicit-any */ +/** + * The context supertype of a middleware type. + */ export type MergedContextOf = MergeContexts>; diff --git a/packages/json-rpc-engine/src/v2/MiddlewareContext.ts b/packages/json-rpc-engine/src/v2/MiddlewareContext.ts index 8eb291dcc7..073b7e0c76 100644 --- a/packages/json-rpc-engine/src/v2/MiddlewareContext.ts +++ b/packages/json-rpc-engine/src/v2/MiddlewareContext.ts @@ -122,6 +122,9 @@ export type MergeContexts = ExcludeNever>>> >; +/** + * A constraint for {@link MiddlewareContext} generic parameters. + */ // Non-polluting `any` constraint. // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ContextConstraint = MiddlewareContext; diff --git a/packages/json-rpc-engine/src/v2/index.ts b/packages/json-rpc-engine/src/v2/index.ts index 29560da7df..ba2c932e42 100644 --- a/packages/json-rpc-engine/src/v2/index.ts +++ b/packages/json-rpc-engine/src/v2/index.ts @@ -4,13 +4,16 @@ export { createScaffoldMiddleware } from './createScaffoldMiddleware'; export { JsonRpcEngineV2 } from './JsonRpcEngineV2'; export type { JsonRpcMiddleware, + MergedContextOf, MiddlewareParams, + MiddlewareConstraint, Next, + RequestOf, ResultConstraint, } from './JsonRpcEngineV2'; export { JsonRpcServer } from './JsonRpcServer'; export { MiddlewareContext } from './MiddlewareContext'; -export type { EmptyContext } from './MiddlewareContext'; +export type { EmptyContext, ContextConstraint } from './MiddlewareContext'; export { isNotification, isRequest, JsonRpcEngineError } from './utils'; export type { Json, From 7aff105a6530da01fda75ed2c5cb09a234f17dae Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:19:12 -0800 Subject: [PATCH 15/19] test: Remove red herring from test matcher --- packages/eth-json-rpc-provider/src/internal-provider.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index ea061cb5e3..605b90245f 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -207,7 +207,6 @@ describe.each([ await expect(async () => provider.request(request)).rejects.toThrow( rpcErrors.internal({ message: 'Test error', - data: { cause: 'Test cause' }, }), ); }); From 35ecbdf3bc3152e251683708b27672c8e28456df Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:19:23 -0800 Subject: [PATCH 16/19] docs: Update changelogs per review feedback --- packages/eth-json-rpc-provider/CHANGELOG.md | 10 ++++++++++ packages/json-rpc-engine/CHANGELOG.md | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index 39ef973fcf..b564b68d55 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `providerFromMiddlewareV2` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - This accepts the new middleware from `@metamask/json-rpc-engine/v2`. + ### Changed - **BREAKING:** Replace `SafeEventEmitterProvider` with `InternalProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) @@ -16,6 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcEngineV2` internally wherever they appear. This change should mostly be unobservable. However, due to differences in error handling, this may be breaking for consumers. +### Deprecated + +- Deprecate `providerFromMiddleware` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - Use `providerFromMiddlewareV2` instead, which supports the new middleware from `@metamask/json-rpc-engine/v2`. + ### Removed - **BREAKING:** Remove `providerFromEngine` ([#7001](https://github.com/MetaMask/core/pull/7001)) diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index b47e9d4981..2f2d9c57f0 100644 --- a/packages/json-rpc-engine/CHANGELOG.md +++ b/packages/json-rpc-engine/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7001](https://github.com/MetaMask/core/pull/7001), [#7032](https://github.com/MetaMask/core/pull/7032)) +- Add `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7001](https://github.com/MetaMask/core/pull/7001), [#7032](https://github.com/MetaMask/core/pull/7032)) - This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation. See the readme for details. ## [10.1.1] From 4ebd3d42dad1bb1019ed717cf1e58f9ee6cb96cb Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:00:55 -0800 Subject: [PATCH 17/19] refactor: Replace uuid with nanoid for internal provider ids --- packages/eth-json-rpc-provider/package.json | 3 +- .../src/internal-provider.test.ts | 38 +++++++++---------- .../src/internal-provider.ts | 4 +- yarn.lock | 2 +- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/eth-json-rpc-provider/package.json b/packages/eth-json-rpc-provider/package.json index e7daf95511..c897f59db8 100644 --- a/packages/eth-json-rpc-provider/package.json +++ b/packages/eth-json-rpc-provider/package.json @@ -55,7 +55,8 @@ "dependencies": { "@metamask/json-rpc-engine": "^10.1.1", "@metamask/rpc-errors": "^7.0.2", - "@metamask/utils": "^11.8.1" + "@metamask/utils": "^11.8.1", + "nanoid": "^3.3.8" }, "devDependencies": { "@ethersproject/providers": "^5.7.0", diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index 605b90245f..6e102647d9 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -117,7 +117,7 @@ describe.each([ }); const provider = new InternalProvider({ engine: rpcHandler }); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', params: { @@ -129,7 +129,7 @@ describe.each([ const result = await provider.request(request); expect(req).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -158,7 +158,7 @@ describe.each([ const result = await provider.request(request); expect(req).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -179,7 +179,7 @@ describe.each([ }); const provider = new InternalProvider({ engine: rpcHandler }); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', }; @@ -199,7 +199,7 @@ describe.each([ }); const provider = new InternalProvider({ engine: rpcHandler }); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', }; @@ -222,7 +222,7 @@ describe.each([ const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', params: { @@ -234,7 +234,7 @@ describe.each([ const response = await promisifiedSendAsync(request); expect(req).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -264,7 +264,7 @@ describe.each([ const response = await promisifiedSendAsync(request); expect(req).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -282,7 +282,7 @@ describe.each([ const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', }; @@ -307,7 +307,7 @@ describe.each([ const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', }; @@ -323,7 +323,7 @@ describe.each([ const rpcHandler = createRpcHandler('test', 42); const provider = new InternalProvider({ engine: rpcHandler }); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', }; @@ -343,7 +343,7 @@ describe.each([ const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSend = promisify(provider.send); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', params: { @@ -355,7 +355,7 @@ describe.each([ const response = await promisifiedSend(request); expect(req).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -385,7 +385,7 @@ describe.each([ const response = await promisifiedSend(request); expect(req).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -403,7 +403,7 @@ describe.each([ const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSend = promisify(provider.send); const request = { - id: 1, + id: '1', jsonrpc: '2.0' as const, method: 'test', }; @@ -426,7 +426,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -443,7 +443,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -461,7 +461,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -477,7 +477,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: expect.any(Number), + id: expect.any(String), jsonrpc: '2.0', method: 'test', }); diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index d8431e15be..b761a5c5db 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -1,6 +1,5 @@ import { asV2Middleware, - getUniqueId, type JsonRpcEngine, } from '@metamask/json-rpc-engine'; import type { @@ -16,6 +15,7 @@ import { type JsonRpcRequest, type JsonRpcVersion2, } from '@metamask/utils'; +import { nanoid } from 'nanoid'; /** * A JSON-RPC request conforming to the EIP-1193 specification. @@ -161,7 +161,7 @@ export function convertEip1193RequestToJsonRpcRequest( eip1193Request: Eip1193Request, ): JsonRpcRequest { const { - id = getUniqueId(), + id = nanoid(), jsonrpc = '2.0', method, params, diff --git a/yarn.lock b/yarn.lock index af6dfe1149..f3537f80cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3612,10 +3612,10 @@ __metadata: ethers: "npm:^6.12.0" jest: "npm:^27.5.1" jest-it-up: "npm:^2.0.2" + nanoid: "npm:^3.3.8" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" typescript: "npm:~5.2.2" - uuid: "npm:^8.3.2" languageName: unknown linkType: soft From 5280dc1e9f37dd71b07e740b05262d0840dd6384 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:05:39 -0800 Subject: [PATCH 18/19] chore: Lint --- .../eth-json-rpc-provider/src/internal-provider.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index b761a5c5db..920416c731 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -1,7 +1,4 @@ -import { - asV2Middleware, - type JsonRpcEngine, -} from '@metamask/json-rpc-engine'; +import { asV2Middleware, type JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { ContextConstraint, MiddlewareContext, @@ -160,12 +157,7 @@ export class InternalProvider { export function convertEip1193RequestToJsonRpcRequest( eip1193Request: Eip1193Request, ): JsonRpcRequest { - const { - id = nanoid(), - jsonrpc = '2.0', - method, - params, - } = eip1193Request; + const { id = nanoid(), jsonrpc = '2.0', method, params } = eip1193Request; return params ? { From 4a63eeab480fd87648bff99b33aa894db713376f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:08:48 -0800 Subject: [PATCH 19/19] docs: Explain the typecasts in provider-from-middleware --- .../eth-json-rpc-provider/src/provider-from-middleware.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts index 33905a62c8..123e2772f2 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts @@ -24,6 +24,10 @@ export function providerFromMiddleware< Result extends Json, >(middleware: LegacyJsonRpcMiddleware): InternalProvider { return providerFromMiddlewareV2( + // This function is generic on the Params and Result types to match the legacy JsonRpcMiddleware type. + // However, since the V2 JsonRpcMiddleware type is not generic on the Params, we need to elide this + // parameter by upcasting the request type to JsonRpcRequest, or we get an error due to contravariance + // since JsonRpcRequest is not assignable to JsonRpcRequest. asV2Middleware(middleware) as JsonRpcMiddleware, ); } @@ -44,6 +48,9 @@ export function providerFromMiddlewareV2< >(middleware: Middleware): InternalProvider { return new InternalProvider({ engine: JsonRpcEngineV2.create({ + // This function is generic in order to accept middleware functions with narrower types than + // the plain JsonRpcMiddleware type. However, since InternalProvider is non-generic, + // we need to upcast the middleware to avoid a type error. middleware: [middleware as JsonRpcMiddleware], }), });