From 069132bb67b6a7abef823c33e596b9ac946821f3 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:50:50 -0700 Subject: [PATCH 01/30] refactor: Rewrite block-cache middleware --- .../src/block-cache.test.ts | 251 ++++++++++-------- .../src/block-cache.ts | 157 ++++++----- .../src/inflight-cache.ts | 17 +- .../eth-json-rpc-middleware/tsconfig.json | 2 +- 4 files changed, 226 insertions(+), 201 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/block-cache.test.ts b/packages/eth-json-rpc-middleware/src/block-cache.test.ts index 1352675e3fa..b5a0558b019 100644 --- a/packages/eth-json-rpc-middleware/src/block-cache.test.ts +++ b/packages/eth-json-rpc-middleware/src/block-cache.test.ts @@ -1,5 +1,8 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcSuccess } from '@metamask/utils'; +import { + JsonRpcEngineV2, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; +import type { Json } from '@metamask/utils'; import { createBlockCacheMiddleware } from '.'; import { @@ -40,32 +43,31 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = `0x${hitCount}`; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return `0x${hitCount}`; + }, + ], }); - const requestWithSkipCache = { - ...createRequest({ - method: 'eth_getBalance', - params: ['0x1234'], - }), - skipCache: true, - }; + const request = createRequest({ + method: 'eth_getBalance', + params: ['0x1234'], + }); + + const context = new MiddlewareContext<{ skipCache?: boolean }>([ + ['skipCache', true], + ]); - const result1 = (await engine.handle( - requestWithSkipCache, - )) as JsonRpcSuccess; - const result2 = (await engine.handle( - requestWithSkipCache, - )) as JsonRpcSuccess; + const result1 = await engine.handle(request, { context }); + const result2 = await engine.handle(request, { context }); expect(hitCount).toBe(2); - expect(result1.result).toBe('0x1'); - expect(result2.result).toBe('0x2'); + expect(result1).toBe('0x1'); + expect(result2).toBe('0x2'); }); it('skips caching methods with Never strategy', async () => { @@ -77,12 +79,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = `0x${hitCount}`; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return `0x${hitCount}`; + }, + ], }); // eth_sendTransaction is a method that is not cacheable @@ -90,12 +94,12 @@ describe('block cache middleware', () => { method: 'eth_sendTransaction', }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(2); - expect(result1.result).toBe('0x1'); - expect(result2.result).toBe('0x2'); + expect(result1).toBe('0x1'); + expect(result2).toBe('0x2'); }); it('skips caching requests with pending blockTag', async () => { @@ -107,12 +111,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = `0x${hitCount}`; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return `0x${hitCount}`; + }, + ], }); const request = createRequest({ @@ -120,12 +126,12 @@ describe('block cache middleware', () => { params: ['0x1234', 'pending'], }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(2); - expect(result1.result).toBe('0x1'); - expect(result2.result).toBe('0x2'); + expect(result1).toBe('0x1'); + expect(result2).toBe('0x2'); }); it('caches requests with cacheable method and valid blockTag', async () => { @@ -138,12 +144,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = `0x${hitCount}`; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return `0x${hitCount}`; + }, + ], }); const request = createRequest({ @@ -151,13 +159,13 @@ describe('block cache middleware', () => { params: ['0x1234', 'latest'], }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(1); expect(getLatestBlockSpy).toHaveBeenCalledTimes(2); - expect(result1.result).toBe('0x1'); - expect(result2.result).toBe('0x1'); + expect(result1).toBe('0x1'); + expect(result2).toBe('0x1'); }); it('defaults cacheable request block tags to "latest"', async () => { @@ -170,12 +178,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = `0x${hitCount}`; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return `0x${hitCount}`; + }, + ], }); const request = createRequest({ @@ -183,13 +193,13 @@ describe('block cache middleware', () => { params: ['0x1234'], }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(1); expect(getLatestBlockSpy).toHaveBeenCalledTimes(2); - expect(result1.result).toBe('0x1'); - expect(result2.result).toBe('0x1'); + expect(result1).toBe('0x1'); + expect(result2).toBe('0x1'); }); it('caches requests with "earliest" block tag', async () => { @@ -202,12 +212,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = `0x${hitCount}`; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return `0x${hitCount}`; + }, + ], }); const request = createRequest({ @@ -215,13 +227,13 @@ describe('block cache middleware', () => { params: ['0x1234', 'earliest'], }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(1); expect(getLatestBlockSpy).not.toHaveBeenCalled(); - expect(result1.result).toBe('0x1'); - expect(result2.result).toBe('0x1'); + expect(result1).toBe('0x1'); + expect(result2).toBe('0x1'); }); it('caches requests with hex block tag', async () => { @@ -234,12 +246,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = `0x${hitCount}`; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return `0x${hitCount}`; + }, + ], }); const request = createRequest({ @@ -247,18 +261,19 @@ describe('block cache middleware', () => { params: ['0x1234', '0x2'], }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(1); expect(getLatestBlockSpy).not.toHaveBeenCalled(); - expect(result1.result).toBe('0x1'); - expect(result2.result).toBe('0x1'); + expect(result1).toBe('0x1'); + expect(result2).toBe('0x1'); }); }); describe('cache strategy edge cases', () => { - it.each([undefined, null, '\u003cnil\u003e'])( + // `undefined` is also an empty value, but returning that causes the engine to throw + it.each([null, '\u003cnil\u003e'])( 'skips caching "empty" result values: %s', async (emptyValue) => { stubProviderRequests(provider, [ @@ -269,12 +284,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = emptyValue; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return emptyValue; + }, + ], }); const request = createRequest({ @@ -282,12 +299,12 @@ describe('block cache middleware', () => { params: ['0x1234'], }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(2); - expect(result1.result).toBe(emptyValue); - expect(result2.result).toBe(emptyValue); + expect(result1).toBe(emptyValue); + expect(result2).toBe(emptyValue); }, ); @@ -302,7 +319,7 @@ describe('block cache middleware', () => { blockHash: '0x0000000000000000000000000000000000000000000000000000000000000000', }, - ] as Json[])('%o', async (result) => { + ] as Json[])('%o', async (expectedResult) => { stubProviderRequests(provider, [ { request: { method: 'eth_blockNumber' }, @@ -311,12 +328,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = result; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return expectedResult; + }, + ], }); const request = createRequest({ @@ -324,12 +343,12 @@ describe('block cache middleware', () => { params: ['0x123'], }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(2); - expect(result1.result).toBe(result); - expect(result2.result).toBe(result); + expect(result1).toBe(expectedResult); + expect(result2).toBe(expectedResult); }); }, ); @@ -347,12 +366,14 @@ describe('block cache middleware', () => { ]); let hitCount = 0; - const engine = new JsonRpcEngine(); - engine.push(createBlockCacheMiddleware({ blockTracker })); - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = `0x${hitCount}`; - end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockCacheMiddleware({ blockTracker }), + () => { + hitCount += 1; + return `0x${hitCount}`; + }, + ], }); const request = createRequest({ @@ -360,13 +381,13 @@ describe('block cache middleware', () => { params: ['0x1234', 'latest'], }); - const result1 = (await engine.handle(request)) as JsonRpcSuccess; - const result2 = (await engine.handle(request)) as JsonRpcSuccess; + const result1 = await engine.handle(request); + const result2 = await engine.handle(request); expect(hitCount).toBe(2); expect(getLatestBlockSpy).toHaveBeenCalledTimes(2); - expect(result1.result).toBe('0x1'); - expect(result2.result).toBe('0x2'); + expect(result1).toBe('0x1'); + expect(result2).toBe('0x2'); }); }); }); diff --git a/packages/eth-json-rpc-middleware/src/block-cache.ts b/packages/eth-json-rpc-middleware/src/block-cache.ts index 0311ebd9999..c2562cb78de 100644 --- a/packages/eth-json-rpc-middleware/src/block-cache.ts +++ b/packages/eth-json-rpc-middleware/src/block-cache.ts @@ -1,5 +1,8 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; +import type { + JsonRpcMiddleware, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { projectLogger, createModuleLogger } from './logging-utils'; @@ -8,7 +11,6 @@ import type { BlockCache, // eslint-disable-next-line @typescript-eslint/no-shadow Cache, - JsonRpcCacheMiddleware, JsonRpcRequestToCache, } from './types'; import { @@ -21,7 +23,7 @@ import { const log = createModuleLogger(projectLogger, 'block-cache'); // `` comes from https://github.com/ethereum/go-ethereum/issues/16925 -const emptyValues = [undefined, null, '\u003cnil\u003e']; +const emptyValues: unknown[] = [undefined, null, '\u003cnil\u003e']; type BlockCacheMiddlewareOptions = { blockTracker?: PollingBlockTracker; @@ -98,7 +100,7 @@ class BlockCacheStrategy { canCacheResult(request: JsonRpcRequest, result: Block): boolean { // never cache empty values (e.g. undefined) - if (emptyValues.includes(result as any)) { + if (emptyValues.includes(result)) { return false; } @@ -134,18 +136,17 @@ class BlockCacheStrategy { export function createBlockCacheMiddleware({ blockTracker, -}: BlockCacheMiddlewareOptions = {}): JsonRpcCacheMiddleware< - JsonRpcParams, - Json +}: BlockCacheMiddlewareOptions = {}): JsonRpcMiddleware< + JsonRpcRequestToCache, + Json, + MiddlewareContext<{ skipCache?: boolean }> > { - // validate options if (!blockTracker) { throw new Error( 'createBlockCacheMiddleware - No PollingBlockTracker specified', ); } - // create caching strategies const blockCache: BlockCacheStrategy = new BlockCacheStrategy(); const strategies: Record = { [CacheStrategy.Permanent]: blockCache, @@ -154,82 +155,72 @@ export function createBlockCacheMiddleware({ [CacheStrategy.Never]: undefined, }; - return createAsyncMiddleware( - async (req: JsonRpcRequestToCache, res, next) => { - // allow cach to be skipped if so specified - if (req.skipCache) { - return next(); - } - // check type and matching strategy - const type = cacheTypeForMethod(req.method); - const strategy = strategies[type]; - // If there's no strategy in place, pass it down the chain. - if (!strategy) { - return next(); - } + return async ({ request, next, context }) => { + if (context.get('skipCache')) { + return next(); + } - // If the strategy can't cache this request, ignore it. - if (!strategy.canCacheRequest(req)) { - return next(); - } + const type = cacheTypeForMethod(request.method); + const strategy = strategies[type]; + if (!strategy) { + return next(); + } - // get block reference (number or keyword) - const requestBlockTag = blockTagForRequest(req); - const blockTag = - requestBlockTag && typeof requestBlockTag === 'string' - ? requestBlockTag - : 'latest'; - - log('blockTag = %o, req = %o', blockTag, req); - - // get exact block number - let requestedBlockNumber: string; - if (blockTag === 'earliest') { - // this just exists for symmetry with "latest" - requestedBlockNumber = '0x00'; - } else if (blockTag === 'latest') { - // fetch latest block number - log('Fetching latest block number to determine cache key'); - const latestBlockNumber = await blockTracker.getLatestBlock(); - // clear all cache before latest block - log( - 'Clearing values stored under block numbers before %o', - latestBlockNumber, - ); - blockCache.clearBefore(latestBlockNumber); - requestedBlockNumber = latestBlockNumber; - } else { - // We have a hex number - requestedBlockNumber = blockTag; - } - // end on a hit, continue on a miss - const cacheResult: Block | undefined = await strategy.get( - req, + if (!strategy.canCacheRequest(request)) { + return next(); + } + + const requestBlockTag = blockTagForRequest(request); + const blockTag = + requestBlockTag && typeof requestBlockTag === 'string' + ? requestBlockTag + : 'latest'; + + log('blockTag = %o, req = %o', blockTag, request); + + // get exact block number + let requestedBlockNumber: string; + if (blockTag === 'earliest') { + // this just exists for symmetry with "latest" + requestedBlockNumber = '0x00'; + } else if (blockTag === 'latest') { + log('Fetching latest block number to determine cache key'); + const latestBlockNumber = await blockTracker.getLatestBlock(); + + // clear all cache before latest block + log( + 'Clearing values stored under block numbers before %o', + latestBlockNumber, + ); + blockCache.clearBefore(latestBlockNumber); + requestedBlockNumber = latestBlockNumber; + } else { + // we have a hex number + requestedBlockNumber = blockTag; + } + + // end on a hit, continue on a miss + const cacheResult = await strategy.get(request, requestedBlockNumber); + if (cacheResult === undefined) { + // cache miss + // wait for other middleware to handle request + log( + 'No cache stored under block number %o, carrying request forward', requestedBlockNumber, ); - if (cacheResult === undefined) { - // cache miss - // wait for other middleware to handle request - log( - 'No cache stored under block number %o, carrying request forward', - requestedBlockNumber, - ); - await next(); - - // add result to cache - // it's safe to cast res.result as Block, due to runtime type checks - // performed when strategy.set is called - log('Populating cache with', res); - await strategy.set(req, requestedBlockNumber, res.result as Block); - } else { - // fill in result from cache - log( - 'Cache hit, reusing cache result stored under block number %o', - requestedBlockNumber, - ); - res.result = cacheResult; - } - return undefined; - }, - ); + const result = await next(); + + // add result to cache + // it's safe to cast res.result as Block, due to runtime type checks + // performed when strategy.set is called + log('Populating cache with', result); + await strategy.set(request, requestedBlockNumber, result as Block); + return result; + } + log( + 'Cache hit, reusing cache result stored under block number %o', + requestedBlockNumber, + ); + return cacheResult; + }; } diff --git a/packages/eth-json-rpc-middleware/src/inflight-cache.ts b/packages/eth-json-rpc-middleware/src/inflight-cache.ts index aa2620135fa..8cd9673e4d7 100644 --- a/packages/eth-json-rpc-middleware/src/inflight-cache.ts +++ b/packages/eth-json-rpc-middleware/src/inflight-cache.ts @@ -1,13 +1,14 @@ -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; +import { createAsyncMiddleware, type JsonRpcMiddleware } from '@metamask/json-rpc-engine'; import type { JsonRpcParams, Json, PendingJsonRpcResponse, + JsonRpcRequest, } from '@metamask/utils'; import { klona } from 'klona/full'; import { projectLogger, createModuleLogger } from './logging-utils'; -import type { JsonRpcRequestToCache, JsonRpcCacheMiddleware } from './types'; +import type { JsonRpcRequestToCache } from './types'; import { cacheIdentifierForRequest } from './utils/cache'; type RequestHandlers = (handledRes: PendingJsonRpcResponse) => void; @@ -15,6 +16,18 @@ type InflightRequest = { [cacheId: string]: RequestHandlers[]; }; +export type JsonRpcCacheMiddleware< + Params extends JsonRpcParams, + Result extends Json, +> = + JsonRpcMiddleware extends ( + req: JsonRpcRequest, + ...args: infer X + ) => infer Y + ? (req: JsonRpcRequestToCache, ...args: X) => Y + : never; + + const log = createModuleLogger(projectLogger, 'inflight-cache'); export function createInflightCacheMiddleware(): JsonRpcCacheMiddleware< diff --git a/packages/eth-json-rpc-middleware/tsconfig.json b/packages/eth-json-rpc-middleware/tsconfig.json index a130e3632c4..1788c61e4fe 100644 --- a/packages/eth-json-rpc-middleware/tsconfig.json +++ b/packages/eth-json-rpc-middleware/tsconfig.json @@ -20,5 +20,5 @@ "path": "../network-controller" } ], - "include": ["../../types", "./src", "./test", "./types"] + "include": ["../../types", "./src", "./test"] } From 97711cbd5cc4e4976d3dd1b93768fe0df8a414cc Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:21:21 -0700 Subject: [PATCH 02/30] refactor: Rewrite block-ref-rewrite --- .../src/block-ref-rewrite.test.ts | 95 ++++++++++--------- .../src/block-ref-rewrite.ts | 28 +++--- packages/eth-json-rpc-middleware/src/types.ts | 11 --- .../test/util/helpers.ts | 40 ++++---- 4 files changed, 87 insertions(+), 87 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/block-ref-rewrite.test.ts b/packages/eth-json-rpc-middleware/src/block-ref-rewrite.test.ts index ad0dcc10212..360524577db 100644 --- a/packages/eth-json-rpc-middleware/src/block-ref-rewrite.test.ts +++ b/packages/eth-json-rpc-middleware/src/block-ref-rewrite.test.ts @@ -1,5 +1,5 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import type { JsonRpcRequest } from '@metamask/utils'; import { createBlockRefRewriteMiddleware } from './block-ref-rewrite'; @@ -27,12 +27,14 @@ describe('createBlockRefRewriteMiddleware', () => { const mockBlockTracker = createMockBlockTracker(); const getLatestBlockSpy = jest.spyOn(mockBlockTracker, 'getLatestBlock'); - const engine = new JsonRpcEngine(); - engine.push( - createBlockRefRewriteMiddleware({ - blockTracker: mockBlockTracker, - }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockRefRewriteMiddleware({ + blockTracker: mockBlockTracker, + }), + createFinalMiddlewareWithDefaultResult(), + ], + }); const originalRequest = createRequest({ method: 'eth_chainId', @@ -48,12 +50,14 @@ describe('createBlockRefRewriteMiddleware', () => { const mockBlockTracker = createMockBlockTracker(); const getLatestBlockSpy = jest.spyOn(mockBlockTracker, 'getLatestBlock'); - const engine = new JsonRpcEngine(); - engine.push( - createBlockRefRewriteMiddleware({ - blockTracker: mockBlockTracker, - }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockRefRewriteMiddleware({ + blockTracker: mockBlockTracker, + }), + createFinalMiddlewareWithDefaultResult(), + ], + }); const originalRequest = createRequest({ method: 'eth_getBalance', @@ -72,20 +76,20 @@ describe('createBlockRefRewriteMiddleware', () => { .spyOn(mockBlockTracker, 'getLatestBlock') .mockResolvedValue('0xabc123'); - const engine = new JsonRpcEngine(); - engine.push( - createBlockRefRewriteMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - // Mock a middleware that captures the request after modification let capturedRequest: JsonRpcRequest | undefined; - engine.push(async (req, _res, next) => { - capturedRequest = { ...req }; - return next(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockRefRewriteMiddleware({ + blockTracker: mockBlockTracker, + }), + async ({ request, next }) => { + capturedRequest = { ...request } as JsonRpcRequest; + return next(); + }, + createFinalMiddlewareWithDefaultResult(), + ], }); - engine.push(createFinalMiddlewareWithDefaultResult()); const originalRequest = createRequest({ method: 'eth_getBalance', @@ -107,17 +111,18 @@ describe('createBlockRefRewriteMiddleware', () => { .spyOn(mockBlockTracker, 'getLatestBlock') .mockResolvedValue('0x111222'); - const engine = new JsonRpcEngine(); - engine.push( - createBlockRefRewriteMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - let capturedRequest: JsonRpcRequest | undefined; - engine.push(async (req, _res, next) => { - capturedRequest = { ...req }; - return next(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockRefRewriteMiddleware({ + blockTracker: mockBlockTracker, + }), + async ({ request, next }) => { + capturedRequest = { ...request } as JsonRpcRequest; + return next(); + }, + createFinalMiddlewareWithDefaultResult(), + ], }); const originalRequest = createRequest({ @@ -140,19 +145,19 @@ describe('createBlockRefRewriteMiddleware', () => { .spyOn(mockBlockTracker, 'getLatestBlock') .mockResolvedValue('0xffffff'); - const engine = new JsonRpcEngine(); - engine.push( - createBlockRefRewriteMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - let capturedRequest: JsonRpcRequest | undefined; - engine.push(async (req, _res, next) => { - capturedRequest = { ...req }; - return next(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockRefRewriteMiddleware({ + blockTracker: mockBlockTracker, + }), + async ({ request, next }) => { + capturedRequest = { ...request } as JsonRpcRequest; + return next(); + }, + createFinalMiddlewareWithDefaultResult(), + ], }); - engine.push(createFinalMiddlewareWithDefaultResult()); const originalRequest = createRequest({ method: 'eth_getBalance', diff --git a/packages/eth-json-rpc-middleware/src/block-ref-rewrite.ts b/packages/eth-json-rpc-middleware/src/block-ref-rewrite.ts index dfbb5ad241a..be2cc882382 100644 --- a/packages/eth-json-rpc-middleware/src/block-ref-rewrite.ts +++ b/packages/eth-json-rpc-middleware/src/block-ref-rewrite.ts @@ -1,7 +1,6 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcParams } from '@metamask/utils'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; import { blockTagParamIndex } from './utils/cache'; @@ -20,7 +19,7 @@ type BlockRefRewriteMiddlewareOptions = { export function createBlockRefRewriteMiddleware({ blockTracker, }: BlockRefRewriteMiddlewareOptions = {}): JsonRpcMiddleware< - JsonRpcParams, + JsonRpcRequest, Json > { if (!blockTracker) { @@ -29,15 +28,17 @@ export function createBlockRefRewriteMiddleware({ ); } - return createAsyncMiddleware(async (req, _res, next) => { - const blockRefIndex: number | undefined = blockTagParamIndex(req.method); + return async ({ request, next }) => { + const blockRefIndex: number | undefined = blockTagParamIndex( + request.method, + ); if (blockRefIndex === undefined) { return next(); } const blockRef: string | undefined = - Array.isArray(req.params) && req.params[blockRefIndex] - ? (req.params[blockRefIndex] as string) + Array.isArray(request.params) && request.params[blockRefIndex] + ? (request.params[blockRefIndex] as string) : // omitted blockRef implies "latest" 'latest'; @@ -47,9 +48,14 @@ export function createBlockRefRewriteMiddleware({ // rewrite blockRef to block-tracker's block number const latestBlockNumber = await blockTracker.getLatestBlock(); - if (Array.isArray(req.params)) { - req.params[blockRefIndex] = latestBlockNumber; + if (Array.isArray(request.params)) { + const params = request.params.slice(); + params[blockRefIndex] = latestBlockNumber; + return next({ + ...request, + params, + }); } return next(); - }); + }; } diff --git a/packages/eth-json-rpc-middleware/src/types.ts b/packages/eth-json-rpc-middleware/src/types.ts index 14070671dd7..17c4381ebb3 100644 --- a/packages/eth-json-rpc-middleware/src/types.ts +++ b/packages/eth-json-rpc-middleware/src/types.ts @@ -11,17 +11,6 @@ export type JsonRpcRequestToCache = skipCache?: boolean; }; -export type JsonRpcCacheMiddleware< - Params extends JsonRpcParams, - Result extends Json, -> = - JsonRpcMiddleware extends ( - req: JsonRpcRequest, - ...args: infer X - ) => infer Y - ? (req: JsonRpcRequestToCache, ...args: X) => Y - : never; - export type BlockData = string | string[]; export type Block = Record; diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index 65308688277..dc620bf6f25 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -2,8 +2,9 @@ import { PollingBlockTracker } from '@metamask/eth-block-tracker'; import { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { JsonRpcEngine, - type JsonRpcMiddleware, + JsonRpcMiddleware as LegacyJsonRpcMiddleware, } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona/full'; import { isDeepStrictEqual } from 'util'; @@ -66,22 +67,15 @@ export type ProviderRequestStub< * @template Result - The type that represents the result. * @returns The created middleware, as a mock function. */ -export function createFinalMiddlewareWithDefaultResult< - Params extends JsonRpcParams, - Result extends Json, ->(): JsonRpcMiddleware { - return jest.fn((req, res, _next, end) => { - if (res.id === undefined) { - res.id = req.id; +export function createFinalMiddlewareWithDefaultResult(): JsonRpcMiddleware { + return jest.fn(async ({ next }) => { + // Not a Node.js callback + // eslint-disable-next-line n/callback-return + const result = await next(); + if (result === undefined) { + return 'default result'; } - - res.jsonrpc ??= '2.0'; - - if (res.result === undefined) { - res.result = 'default result'; - } - - end(); + return result; }); } @@ -91,7 +85,10 @@ export function createFinalMiddlewareWithDefaultResult< * * @returns The provider and block tracker. */ -export function createProviderAndBlockTracker() { +export function createProviderAndBlockTracker(): { + provider: InternalProvider; + blockTracker: PollingBlockTracker; +} { const engine = new JsonRpcEngine(); const provider = new InternalProvider({ engine }); @@ -112,13 +109,16 @@ export function createProviderAndBlockTracker() { * @returns The created engine. */ export function createEngine( - middlewareUnderTest: JsonRpcMiddleware, - ...otherMiddleware: JsonRpcMiddleware[] + middlewareUnderTest: LegacyJsonRpcMiddleware, + ...otherMiddleware: LegacyJsonRpcMiddleware[] ): JsonRpcEngine { const engine = new JsonRpcEngine(); engine.push(middlewareUnderTest); if (otherMiddleware.length === 0) { - otherMiddleware.push(createFinalMiddlewareWithDefaultResult()); + otherMiddleware.push((_req, res, _next, end) => { + res.result = 'default result'; + end(); + }); } for (const middleware of otherMiddleware) { engine.push(middleware); From 7c06f11af0418e2c8b0347cb3cadb01206a87884 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:44:00 -0700 Subject: [PATCH 03/30] refactor: Rewrite block-ref --- eslint-warning-thresholds.json | 2 +- .../src/block-ref.test.ts | 112 ++++++++---------- .../eth-json-rpc-middleware/src/block-ref.ts | 34 +++--- .../test/util/helpers.ts | 32 ++--- 4 files changed, 83 insertions(+), 97 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 9697b47c0f4..f688909c5c3 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -165,7 +165,7 @@ "jest/expect-expect": 2 }, "packages/eth-json-rpc-middleware/src/block-ref.ts": { - "jsdoc/require-jsdoc": 1 + "jsdoc/match-description": 1 }, "packages/eth-json-rpc-middleware/src/block-tracker-inspector.ts": { "jsdoc/match-description": 1, diff --git a/packages/eth-json-rpc-middleware/src/block-ref.test.ts b/packages/eth-json-rpc-middleware/src/block-ref.test.ts index 6ea0d7db0ea..45d215abef4 100644 --- a/packages/eth-json-rpc-middleware/src/block-ref.test.ts +++ b/packages/eth-json-rpc-middleware/src/block-ref.test.ts @@ -1,3 +1,4 @@ +import { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; import { createBlockRefMiddleware } from '.'; import { createMockParamsWithBlockParamAt, @@ -9,6 +10,7 @@ import { expectProviderRequestNotToHaveBeenMade, createProviderAndBlockTracker, createEngine, + createRequest, } from '../test/util/helpers'; describe('createBlockRefMiddleware', () => { @@ -55,15 +57,14 @@ describe('createBlockRefMiddleware', () => { }), ); - const request = { - id: 1, - jsonrpc: '2.0' as const, + const request = createRequest({ method, params: createMockParamsWithBlockParamAt( blockParamIndex, 'latest', ), - }; + }); + stubProviderRequests(provider, [ createStubForBlockNumberRequest('0x100'), createStubForGenericRequest({ @@ -78,13 +79,9 @@ describe('createBlockRefMiddleware', () => { }), ]); - const response = await engine.handle(request); + const result = await engine.handle(request); - expect(response).toStrictEqual({ - id: 1, - jsonrpc: '2.0', - result: 'something', - }); + expect(result).toBe('something'); }); it('does not proceed to the next middleware after making a request through the provider', async () => { @@ -98,15 +95,14 @@ describe('createBlockRefMiddleware', () => { finalMiddleware, ); - const request = { - id: 1, - jsonrpc: '2.0' as const, + const request = createRequest({ method, params: createMockParamsWithBlockParamAt( blockParamIndex, 'latest', ), - }; + }); + stubProviderRequests(provider, [ createStubForBlockNumberRequest('0x100'), createStubForGenericRequest({ @@ -136,12 +132,11 @@ describe('createBlockRefMiddleware', () => { }), ); - const request = { - jsonrpc: '2.0' as const, - id: 1, + const request = createRequest({ method, params: createMockParamsWithoutBlockParamAt(blockParamIndex), - }; + }); + stubProviderRequests(provider, [ createStubForBlockNumberRequest('0x100'), createStubForGenericRequest({ @@ -156,13 +151,9 @@ describe('createBlockRefMiddleware', () => { }), ]); - const response = await engine.handle(request); + const result = await engine.handle(request); - expect(response).toStrictEqual({ - id: 1, - jsonrpc: '2.0', - result: 'something', - }); + expect(result).toBe('something'); }); it('does not proceed to the next middleware after making a request through the provider', async () => { @@ -176,12 +167,11 @@ describe('createBlockRefMiddleware', () => { finalMiddleware, ); - const request = { - id: 1, - jsonrpc: '2.0' as const, + const request = createRequest({ method, params: createMockParamsWithoutBlockParamAt(blockParamIndex), - }; + }); + stubProviderRequests(provider, [ createStubForBlockNumberRequest('0x100'), createStubForGenericRequest({ @@ -216,15 +206,14 @@ describe('createBlockRefMiddleware', () => { finalMiddleware, ); - const request = { - id: 1, - jsonrpc: '2.0' as const, + const request = createRequest({ method, params: createMockParamsWithBlockParamAt( blockParamIndex, blockParam, ), - }; + }); + const requestSpy = stubProviderRequests(provider, [ createStubForBlockNumberRequest('0x100'), ]); @@ -249,27 +238,26 @@ describe('createBlockRefMiddleware', () => { createStubForBlockNumberRequest('0x100'), ]); - await engine.handle({ - id: 1, - jsonrpc: '2.0' as const, - method, - params: createMockParamsWithBlockParamAt( - blockParamIndex, - blockParam, - ), - }); - - expect(finalMiddleware).toHaveBeenCalledWith( - expect.objectContaining({ + await engine.handle( + createRequest({ + method, params: createMockParamsWithBlockParamAt( blockParamIndex, blockParam, ), }), - expect.anything(), - expect.anything(), - expect.anything(), ); + + expect(finalMiddleware).toHaveBeenCalledWith({ + request: expect.objectContaining({ + params: createMockParamsWithBlockParamAt( + blockParamIndex, + blockParam, + ), + }), + context: expect.any(MiddlewareContext), + next: expect.any(Function), + }); }); }, ); @@ -289,12 +277,11 @@ describe('createBlockRefMiddleware', () => { finalMiddleware, ); - const request = { - id: 1, - jsonrpc: '2.0' as const, + const request = createRequest({ method: 'a_non_block_param_method', params: ['some value', '0x200'], - }; + }); + const requestSpy = stubProviderRequests(provider, [ createStubForBlockNumberRequest('0x100'), ]); @@ -319,21 +306,20 @@ describe('createBlockRefMiddleware', () => { createStubForBlockNumberRequest('0x100'), ]); - await engine.handle({ - id: 1, - jsonrpc: '2.0' as const, - method: 'a_non_block_param_method', - params: ['some value', '0x200'], - }); - - expect(finalMiddleware).toHaveBeenCalledWith( - expect.objectContaining({ + await engine.handle( + createRequest({ + method: 'a_non_block_param_method', params: ['some value', '0x200'], }), - expect.anything(), - expect.anything(), - expect.anything(), ); + + expect(finalMiddleware).toHaveBeenCalledWith({ + request: expect.objectContaining({ + params: ['some value', '0x200'], + }), + context: expect.any(MiddlewareContext), + next: expect.any(Function), + }); }); }); }); diff --git a/packages/eth-json-rpc-middleware/src/block-ref.ts b/packages/eth-json-rpc-middleware/src/block-ref.ts index a5071e9aff2..1fd52d1504c 100644 --- a/packages/eth-json-rpc-middleware/src/block-ref.ts +++ b/packages/eth-json-rpc-middleware/src/block-ref.ts @@ -1,9 +1,8 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcParams } from '@metamask/utils'; -import { klona } from 'klona/full'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +import { klona } from 'klona'; import { projectLogger, createModuleLogger } from './logging-utils'; import type { Block } from './types'; @@ -16,10 +15,19 @@ type BlockRefMiddlewareOptions = { const log = createModuleLogger(projectLogger, 'block-ref'); +/** + * Creates a middleware that rewrites "latest" block references to the known + * latest block number from a block tracker. + * + * @param options - The options for the middleware. + * @param options.provider - The provider to use. + * @param options.blockTracker - The block tracker to use. + * @returns The middleware. + */ export function createBlockRefMiddleware({ provider, blockTracker, -}: BlockRefMiddlewareOptions = {}): JsonRpcMiddleware { +}: BlockRefMiddlewareOptions = {}): JsonRpcMiddleware { if (!provider) { throw Error('BlockRefMiddleware - mandatory "provider" option is missing.'); } @@ -30,16 +38,16 @@ export function createBlockRefMiddleware({ ); } - return createAsyncMiddleware(async (req, res, next) => { - const blockRefIndex = blockTagParamIndex(req.method); + return async ({ request, next }) => { + const blockRefIndex = blockTagParamIndex(request.method); // skip if method does not include blockRef if (blockRefIndex === undefined) { return next(); } - const blockRef = Array.isArray(req.params) - ? (req.params[blockRefIndex] ?? 'latest') + const blockRef = Array.isArray(request.params) + ? (request.params[blockRefIndex] ?? 'latest') : 'latest'; // skip if not "latest" @@ -55,7 +63,7 @@ export function createBlockRefMiddleware({ ); // create child request with specific block-ref - const childRequest = klona(req); + const childRequest = klona(request); if (Array.isArray(childRequest.params)) { childRequest.params[blockRefIndex] = latestBlockNumber; @@ -64,8 +72,6 @@ export function createBlockRefMiddleware({ // perform child request log('Performing another request %o', childRequest); // copy child result onto original response - res.result = await provider.request(childRequest); - - return undefined; - }); + return await provider.request(childRequest); + }; } diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index dc620bf6f25..f1a88ac5693 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -1,9 +1,7 @@ import { PollingBlockTracker } from '@metamask/eth-block-tracker'; import { InternalProvider } from '@metamask/eth-json-rpc-provider'; -import { - JsonRpcEngine, - JsonRpcMiddleware as LegacyJsonRpcMiddleware, -} from '@metamask/json-rpc-engine'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona/full'; @@ -109,21 +107,17 @@ export function createProviderAndBlockTracker(): { * @returns The created engine. */ export function createEngine( - middlewareUnderTest: LegacyJsonRpcMiddleware, - ...otherMiddleware: LegacyJsonRpcMiddleware[] -): JsonRpcEngine { - const engine = new JsonRpcEngine(); - engine.push(middlewareUnderTest); - if (otherMiddleware.length === 0) { - otherMiddleware.push((_req, res, _next, end) => { - res.result = 'default result'; - end(); - }); - } - for (const middleware of otherMiddleware) { - engine.push(middleware); - } - return engine; + middlewareUnderTest: JsonRpcMiddleware, + ...otherMiddleware: JsonRpcMiddleware[] +): JsonRpcEngineV2 { + return JsonRpcEngineV2.create({ + middleware: [ + middlewareUnderTest, + ...(otherMiddleware.length === 0 + ? [createFinalMiddlewareWithDefaultResult()] + : otherMiddleware), + ], + }); } /** From b289e7e6ef657d721b68c3fe5ff11adb3bcaf1e4 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:00:40 -0700 Subject: [PATCH 04/30] refactor: Rewrite block-tracker-inspector --- eslint-warning-thresholds.json | 3 +- .../src/block-tracker-inspector.test.ts | 248 ++++++++---------- .../src/block-tracker-inspector.ts | 71 +++-- 3 files changed, 145 insertions(+), 177 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index f688909c5c3..31e35d990a0 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -168,8 +168,7 @@ "jsdoc/match-description": 1 }, "packages/eth-json-rpc-middleware/src/block-tracker-inspector.ts": { - "jsdoc/match-description": 1, - "jsdoc/require-jsdoc": 1 + "jsdoc/match-description": 2 }, "packages/eth-json-rpc-middleware/src/fetch.test.ts": { "jsdoc/match-description": 1 diff --git a/packages/eth-json-rpc-middleware/src/block-tracker-inspector.test.ts b/packages/eth-json-rpc-middleware/src/block-tracker-inspector.test.ts index e7daa8118aa..8954d2bc315 100644 --- a/packages/eth-json-rpc-middleware/src/block-tracker-inspector.test.ts +++ b/packages/eth-json-rpc-middleware/src/block-tracker-inspector.test.ts @@ -1,5 +1,6 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; +import { rpcErrors } from '@metamask/rpc-errors'; import { createBlockTrackerInspectorMiddleware } from './block-tracker-inspector'; import { @@ -27,19 +28,16 @@ describe('createBlockTrackerInspectorMiddleware', () => { 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - engine.push((_req, res, _next, end) => { - res.result = { - blockNumber: '0x123', // Same as current block - hash: '0xabc', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => ({ + blockNumber: '0x123', // Same as current block + hash: '0xabc', + }), + ], }); const request = createRequest({ @@ -64,19 +62,16 @@ describe('createBlockTrackerInspectorMiddleware', () => { 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - engine.push((_req, res, _next, end) => { - res.result = { - blockNumber: '0x123', // Same as current block - transactionHash: '0xdef', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => ({ + blockNumber: '0x123', // Same as current block + transactionHash: '0xdef', + }), + ], }); const request = createRequest({ @@ -101,13 +96,14 @@ describe('createBlockTrackerInspectorMiddleware', () => { 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - engine.push(createFinalMiddlewareWithDefaultResult()); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + createFinalMiddlewareWithDefaultResult(), + ], + }); const request = createRequest({ method: 'eth_chainId', // Not in futureBlockRefRequests @@ -129,24 +125,19 @@ describe('createBlockTrackerInspectorMiddleware', () => { 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - engine.push((_req, res, _next, end) => { - res.result = { - blockNumber: '0x200', // Higher than current block (0x100) - hash: '0xabc', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => ({ + blockNumber: '0x200', // Higher than current block (0x100) + hash: '0xabc', + }), + ], }); const request = createRequest({ - id: 1, - jsonrpc: '2.0', method: 'eth_getTransactionByHash', params: ['0xhash'], }); @@ -164,19 +155,16 @@ describe('createBlockTrackerInspectorMiddleware', () => { 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - engine.push((_req, res, _next, end) => { - res.result = { - blockNumber: '0x100', // Equals current block (0x100) - hash: '0xabc', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => ({ + blockNumber: '0x100', // Equals current block (0x100) + hash: '0xabc', + }), + ], }); const request = createRequest({ @@ -197,19 +185,16 @@ describe('createBlockTrackerInspectorMiddleware', () => { 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - engine.push((_req, res, _next, end) => { - res.result = { - blockNumber: '0x100', // Lower than current block (0x200) - hash: '0xabc', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => ({ + blockNumber: '0x100', // Lower than current block (0x200) + hash: '0xabc', + }), + ], }); const request = createRequest({ @@ -232,20 +217,16 @@ describe('createBlockTrackerInspectorMiddleware', () => { 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - // Add a middleware that provides a block number - engine.push((_req, res, _next, end) => { - res.result = { - blockNumber: '0x100', - hash: '0xabc', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => ({ + blockNumber: '0x100', + hash: '0xabc', + }), + ], }); const request = createRequest({ @@ -271,19 +252,15 @@ describe('createBlockTrackerInspectorMiddleware', () => { mockBlockTracker, 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - engine.push((_req, res, _next, end) => { - res.error = { - code: -32000, - message: 'Internal error', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => { + throw rpcErrors.internal('Internal error'); + }, + ], }); const request = createRequest({ @@ -291,7 +268,9 @@ describe('createBlockTrackerInspectorMiddleware', () => { params: ['0xhash'], }); - await engine.handle(request); + await expect(engine.handle(request)).rejects.toThrow( + rpcErrors.internal('Internal error'), + ); expect(getCurrentBlockSpy).not.toHaveBeenCalled(); expect(checkForLatestBlockSpy).not.toHaveBeenCalled(); @@ -312,16 +291,13 @@ describe('createBlockTrackerInspectorMiddleware', () => { mockBlockTracker, 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - engine.push((_req, res, _next, end) => { - res.result = result; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => result, + ], }); const request = createRequest({ @@ -345,19 +321,17 @@ describe('createBlockTrackerInspectorMiddleware', () => { mockBlockTracker, 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - engine.push((_req, res, _next, end) => { - res.result = { - blockNumber: 123, - hash: '0xabc', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => ({ + blockNumber: 123, + hash: '0xabc', + }), + ], }); const request = createRequest({ @@ -379,20 +353,16 @@ describe('createBlockTrackerInspectorMiddleware', () => { 'checkForLatestBlock', ); - const engine = new JsonRpcEngine(); - engine.push( - createBlockTrackerInspectorMiddleware({ - blockTracker: mockBlockTracker, - }), - ); - - // Add a middleware that provides malformed hex - engine.push((_req, res, _next, end) => { - res.result = { - blockNumber: 'not-a-hex-number', - hash: '0xabc', - }; - return end(); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createBlockTrackerInspectorMiddleware({ + blockTracker: mockBlockTracker, + }), + () => ({ + blockNumber: 'not-a-hex-number', + hash: '0xabc', + }), + ], }); const request = createRequest({ diff --git a/packages/eth-json-rpc-middleware/src/block-tracker-inspector.ts b/packages/eth-json-rpc-middleware/src/block-tracker-inspector.ts index 63f9f500ee1..a8885ace130 100644 --- a/packages/eth-json-rpc-middleware/src/block-tracker-inspector.ts +++ b/packages/eth-json-rpc-middleware/src/block-tracker-inspector.ts @@ -1,12 +1,7 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import { hasProperty } from '@metamask/utils'; -import type { - Json, - JsonRpcParams, - PendingJsonRpcResponse, -} from '@metamask/utils'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; import { projectLogger, createModuleLogger } from './logging-utils'; @@ -28,41 +23,46 @@ export function createBlockTrackerInspectorMiddleware({ blockTracker, }: { blockTracker: PollingBlockTracker; -}): JsonRpcMiddleware { - return createAsyncMiddleware(async (req, res, next) => { - if (!futureBlockRefRequests.includes(req.method)) { +}): JsonRpcMiddleware { + return async ({ request, next }) => { + if (!futureBlockRefRequests.includes(request.method)) { return next(); } - await next(); + const result = await next(); - const responseBlockNumber = getResultBlockNumber(res); - if (!responseBlockNumber) { - return undefined; - } - - log('res.result.blockNumber exists, proceeding. res = %o', res); + const responseBlockNumber = getResultBlockNumber(result); + if (responseBlockNumber) { + log('res.result.blockNumber exists, proceeding. res = %o', result); - // If number is higher, suggest block-tracker check for a new block - const blockNumber: number = Number.parseInt(responseBlockNumber, 16); - const currentBlockNumber: number = Number.parseInt( - // Typecast: If getCurrentBlock returns null, currentBlockNumber will be NaN, which is fine. - blockTracker.getCurrentBlock() as string, - 16, - ); - if (blockNumber > currentBlockNumber) { - log( - 'blockNumber from response is greater than current block number, refreshing current block number', + // If number is higher, suggest block-tracker check for a new block + const blockNumber: number = Number.parseInt(responseBlockNumber, 16); + const currentBlockNumber: number = Number.parseInt( + // Typecast: If getCurrentBlock returns null, currentBlockNumber will be NaN, which is fine. + blockTracker.getCurrentBlock() as string, + 16, ); - await blockTracker.checkForLatestBlock(); + + if (blockNumber > currentBlockNumber) { + log( + 'blockNumber from response is greater than current block number, refreshing current block number', + ); + await blockTracker.checkForLatestBlock(); + } } - return undefined; - }); + return result; + }; } +/** + * Extracts the block number from the result. + * + * @param result - The result to extract the block number from. + * @returns The block number, or undefined if the result is not an object with a + * `blockNumber` property. + */ function getResultBlockNumber( - response: PendingJsonRpcResponse, + result: Readonly | undefined, ): string | undefined { - const { result } = response; if ( !result || typeof result !== 'object' || @@ -71,8 +71,7 @@ function getResultBlockNumber( return undefined; } - if (typeof result.blockNumber === 'string') { - return result.blockNumber; - } - return undefined; + return typeof result.blockNumber === 'string' + ? result.blockNumber + : undefined; } From c0274a787840661270533cf234904db821e968f4 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:13:43 -0700 Subject: [PATCH 05/30] refactor: Rewrite fetch --- .../eth-json-rpc-middleware/src/fetch.test.ts | 215 +++++++++--------- packages/eth-json-rpc-middleware/src/fetch.ts | 81 +++---- 2 files changed, 142 insertions(+), 154 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/fetch.test.ts b/packages/eth-json-rpc-middleware/src/fetch.test.ts index 8ed25fd9f8e..4ff11297403 100644 --- a/packages/eth-json-rpc-middleware/src/fetch.test.ts +++ b/packages/eth-json-rpc-middleware/src/fetch.test.ts @@ -1,25 +1,34 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngineV2, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; +import { rpcErrors } from '@metamask/rpc-errors'; import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { createFetchMiddleware } from './fetch'; import type { AbstractRpcServiceLike } from './types'; +import { createRequest } from '../test/util/helpers'; describe('createFetchMiddleware', () => { it('calls the RPC service with the correct request headers and body when no `originHttpHeaderKey` option given', async () => { const rpcService = createRpcService(); const requestSpy = jest.spyOn(rpcService, 'request'); - const middleware = createFetchMiddleware({ - rpcService, - }); - const engine = new JsonRpcEngine(); - engine.push(middleware); - await engine.handle({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], + const engine = JsonRpcEngineV2.create({ + middleware: [ + createFetchMiddleware({ + rpcService, + }), + ], }); + await engine.handle( + createRequest({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }), + ); expect(requestSpy).toHaveBeenCalledWith( { @@ -37,24 +46,30 @@ describe('createFetchMiddleware', () => { it('includes the `origin` from the given request in the request headers under the given `originHttpHeaderKey`', async () => { const rpcService = createRpcService(); const requestSpy = jest.spyOn(rpcService, 'request'); - const middleware = createFetchMiddleware({ - rpcService, - options: { - originHttpHeaderKey: 'X-Dapp-Origin', - }, + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createFetchMiddleware({ + rpcService, + options: { + originHttpHeaderKey: 'X-Dapp-Origin', + }, + }), + ], }); + const context = new MiddlewareContext<{ origin: string }>([ + ['origin', 'somedapp.com'], + ]); - const engine = new JsonRpcEngine(); - engine.push(middleware); - // Type assertion: This isn't really a proper JSON-RPC request, but we have - // to get `json-rpc-engine` to think it is. - await engine.handle({ - id: 1, - jsonrpc: '2.0' as const, - method: 'eth_chainId', - params: [], - origin: 'somedapp.com', - } as JsonRpcRequest); + await engine.handle( + createRequest({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }), + { context }, + ); expect(requestSpy).toHaveBeenCalledWith( { @@ -79,24 +94,22 @@ describe('createFetchMiddleware', () => { jsonrpc: '2.0', result: 'the result', }); - const middleware = createFetchMiddleware({ - rpcService, - }); - const engine = new JsonRpcEngine(); - engine.push(middleware); - const result = await engine.handle({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }); - - expect(result).toStrictEqual({ - id: 1, - jsonrpc: '2.0', - result: 'the result', + const engine = JsonRpcEngineV2.create({ + middleware: [ + createFetchMiddleware({ + rpcService, + }), + ], }); + const result = await engine.handle( + createRequest({ + method: 'eth_chainId', + params: [], + }), + ); + + expect(result).toBe('the result'); }); }); @@ -111,33 +124,29 @@ describe('createFetchMiddleware', () => { message: 'oops', }, }); - const middleware = createFetchMiddleware({ - rpcService, - }); - - const engine = new JsonRpcEngine(); - engine.push(middleware); - const result = await engine.handle({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], + const engine = JsonRpcEngineV2.create({ + middleware: [ + createFetchMiddleware({ + rpcService, + }), + ], }); - expect(result).toStrictEqual({ - id: 1, - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal JSON-RPC error.', - stack: expect.stringContaining('Internal JSON-RPC error.'), + await expect( + engine.handle( + createRequest({ + method: 'eth_chainId', + params: [], + }), + ), + ).rejects.toThrow( + rpcErrors.internal({ data: { code: -1000, message: 'oops', - cause: null, }, - }, - }); + }), + ); }); }); @@ -160,26 +169,23 @@ describe('createFetchMiddleware', () => { 'RuntimeError: VM Exception while processing transaction: revert at exactimate (/Users/elliot/code/metamask/metamask-mobile/node_modules/ganache/dist/node/webpack:/Ganache/ethereum/ethereum/lib/src/helpers/gas-estimator.js:257:23)', }, }); - const middleware = createFetchMiddleware({ - rpcService, + const engine = JsonRpcEngineV2.create({ + middleware: [ + createFetchMiddleware({ + rpcService, + }), + ], }); - const engine = new JsonRpcEngine(); - engine.push(middleware); - const result = await engine.handle({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }); - - expect(result).toStrictEqual({ - id: 1, - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal JSON-RPC error.', - stack: expect.stringContaining('Internal JSON-RPC error.'), + await expect( + engine.handle( + createRequest({ + method: 'eth_chainId', + params: [], + }), + ), + ).rejects.toThrow( + rpcErrors.internal({ data: { code: -32000, data: { @@ -189,10 +195,9 @@ describe('createFetchMiddleware', () => { name: 'RuntimeError', stack: 'RuntimeError: VM Exception while processing transaction: revert at exactimate (/Users/elliot/code/metamask/metamask-mobile/node_modules/ganache/dist/node/webpack:/Ganache/ethereum/ethereum/lib/src/helpers/gas-estimator.js:257:23)', - cause: null, }, - }, - }); + }), + ); }); }); @@ -200,33 +205,27 @@ describe('createFetchMiddleware', () => { it('returns an unsuccessful JSON-RPC response containing the error', async () => { const rpcService = createRpcService(); jest.spyOn(rpcService, 'request').mockRejectedValue(new Error('oops')); - const middleware = createFetchMiddleware({ - rpcService, - }); - const engine = new JsonRpcEngine(); - engine.push(middleware); - const result = await engine.handle({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], + const engine = JsonRpcEngineV2.create({ + middleware: [ + createFetchMiddleware({ + rpcService, + }), + ], }); - expect(result).toStrictEqual({ - id: 1, - jsonrpc: '2.0', - error: { - code: -32603, + await expect( + engine.handle( + createRequest({ + method: 'eth_chainId', + params: [], + }), + ), + ).rejects.toThrow( + rpcErrors.internal({ message: 'oops', - data: { - cause: { - message: 'oops', - stack: expect.stringContaining('Error: oops'), - }, - }, - }, - }); + }), + ); }); }); }); diff --git a/packages/eth-json-rpc-middleware/src/fetch.ts b/packages/eth-json-rpc-middleware/src/fetch.ts index 9b673460947..ff6bf2d92b8 100644 --- a/packages/eth-json-rpc-middleware/src/fetch.ts +++ b/packages/eth-json-rpc-middleware/src/fetch.ts @@ -1,19 +1,13 @@ -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; +import type { + JsonRpcMiddleware, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; import { rpcErrors } from '@metamask/rpc-errors'; -import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; +import { klona } from 'klona'; import type { AbstractRpcServiceLike } from './types'; -/** - * Like a JSON-RPC request, but includes an optional `origin` property. - * This will be included in the request as a header if specified. - */ -type JsonRpcRequestWithOrigin = - JsonRpcRequest & { - origin?: string; - }; - /** * Creates middleware for sending a JSON-RPC request through the given RPC * service. @@ -34,41 +28,36 @@ export function createFetchMiddleware({ options?: { originHttpHeaderKey?: string; }; -}): JsonRpcMiddleware { - return createAsyncMiddleware( - async (req: JsonRpcRequestWithOrigin, res) => { - const headers = - 'originHttpHeaderKey' in options && - options.originHttpHeaderKey !== undefined && - req.origin !== undefined - ? { [options.originHttpHeaderKey]: req.origin } - : {}; +}): JsonRpcMiddleware< + JsonRpcRequest, + Json, + MiddlewareContext<{ origin: string }> +> { + return async ({ request, context }) => { + const origin = context.get('origin'); + const headers = + 'originHttpHeaderKey' in options && + options.originHttpHeaderKey !== undefined && + origin !== undefined + ? { [options.originHttpHeaderKey]: origin } + : {}; - const jsonRpcResponse = await rpcService.request( - { - id: req.id, - jsonrpc: req.jsonrpc, - method: req.method, - params: req.params, - }, - { - headers, - }, - ); + const jsonRpcResponse = await rpcService.request(klona(request), { + headers, + }); - // NOTE: We intentionally do not test to see if `jsonRpcResponse.error` is - // strictly a JSON-RPC error response as per - // to account for - // Ganache returning error objects with extra properties such as `name` - if ('error' in jsonRpcResponse) { - throw rpcErrors.internal({ - data: jsonRpcResponse.error, - }); - } + // NOTE: We intentionally do not test to see if `jsonRpcResponse.error` is + // strictly a JSON-RPC error response as per + // to account for + // Ganache returning error objects with extra properties such as `name` + if ('error' in jsonRpcResponse) { + throw rpcErrors.internal({ + data: jsonRpcResponse.error, + }); + } - // Discard the `id` and `jsonrpc` fields in the response body - // (the JSON-RPC engine will fill those in) - res.result = jsonRpcResponse.result; - }, - ); + // Discard the `id` and `jsonrpc` fields in the response body + // (the JSON-RPC engine will fill those in) + return jsonRpcResponse.result; + }; } From 3a3883e065dbfca444994b6dc34b362228bb0dab Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:09:25 -0700 Subject: [PATCH 06/30] refactor: Rewrite inflight-cache --- eslint-warning-thresholds.json | 3 +- .../src/block-cache.ts | 3 +- .../src/inflight-cache.test.ts | 60 +++--- .../src/inflight-cache.ts | 202 ++++++++++-------- packages/eth-json-rpc-middleware/src/types.ts | 5 - 5 files changed, 140 insertions(+), 133 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 31e35d990a0..3efe1f8c171 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -177,8 +177,7 @@ "jsdoc/match-description": 1 }, "packages/eth-json-rpc-middleware/src/inflight-cache.ts": { - "@typescript-eslint/no-explicit-any": 1, - "jsdoc/require-jsdoc": 4 + "jsdoc/match-description": 4 }, "packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts": { "jsdoc/require-jsdoc": 1 diff --git a/packages/eth-json-rpc-middleware/src/block-cache.ts b/packages/eth-json-rpc-middleware/src/block-cache.ts index c2562cb78de..cebc9e5d716 100644 --- a/packages/eth-json-rpc-middleware/src/block-cache.ts +++ b/packages/eth-json-rpc-middleware/src/block-cache.ts @@ -11,7 +11,6 @@ import type { BlockCache, // eslint-disable-next-line @typescript-eslint/no-shadow Cache, - JsonRpcRequestToCache, } from './types'; import { cacheIdentifierForRequest, @@ -137,7 +136,7 @@ class BlockCacheStrategy { export function createBlockCacheMiddleware({ blockTracker, }: BlockCacheMiddlewareOptions = {}): JsonRpcMiddleware< - JsonRpcRequestToCache, + JsonRpcRequest, Json, MiddlewareContext<{ skipCache?: boolean }> > { diff --git a/packages/eth-json-rpc-middleware/src/inflight-cache.test.ts b/packages/eth-json-rpc-middleware/src/inflight-cache.test.ts index a9aa671f8e1..4f797961f0e 100644 --- a/packages/eth-json-rpc-middleware/src/inflight-cache.test.ts +++ b/packages/eth-json-rpc-middleware/src/inflight-cache.test.ts @@ -1,44 +1,44 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import pify from 'pify'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import { createInflightCacheMiddleware } from '.'; +import { createRequest } from '../test/util/helpers'; describe('inflight cache', () => { it('should cache an inflight request and only hit provider once', async () => { - const engine = new JsonRpcEngine(); let hitCount = 0; - - // add inflight cache - engine.push(createInflightCacheMiddleware()); - - // add stalling result handler for `test_blockCache` - engine.push((_req, res, _next, end) => { - hitCount += 1; - res.result = true; - // eslint-disable-next-line jest/no-conditional-in-test - if (hitCount === 1) { - setTimeout(() => end(), 100); - } + const engine = JsonRpcEngineV2.create({ + middleware: [ + createInflightCacheMiddleware(), + async () => { + hitCount += 1; + // eslint-disable-next-line jest/no-conditional-in-test + if (hitCount === 1) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + return true; + }, + ], }); const results = await Promise.all([ - pify(engine.handle).call(engine, { - id: 1, - jsonrpc: '2.0', - method: 'test_blockCache', - params: [], - }), - pify(engine.handle).call(engine, { - id: 2, - jsonrpc: '2.0', - method: 'test_blockCache', - params: [], - }), + engine.handle( + createRequest({ + id: 1, + method: 'test_blockCache', + params: [], + }), + ), + engine.handle( + createRequest({ + id: 2, + method: 'test_blockCache', + params: [], + }), + ), ]); - expect(results[0].result).toBe(true); - expect(results[1].result).toBe(true); - expect(results[0]).not.toStrictEqual(results[1]); // make sure they are unique responses + expect(results[0]).toBe(true); + expect(results[1]).toBe(true); expect(hitCount).toBe(1); // check result handler was only hit once }); }); diff --git a/packages/eth-json-rpc-middleware/src/inflight-cache.ts b/packages/eth-json-rpc-middleware/src/inflight-cache.ts index 8cd9673e4d7..e6062678c8e 100644 --- a/packages/eth-json-rpc-middleware/src/inflight-cache.ts +++ b/packages/eth-json-rpc-middleware/src/inflight-cache.ts @@ -1,125 +1,139 @@ -import { createAsyncMiddleware, type JsonRpcMiddleware } from '@metamask/json-rpc-engine'; import type { - JsonRpcParams, - Json, - PendingJsonRpcResponse, - JsonRpcRequest, + JsonRpcMiddleware, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; +import { + type Json, + type JsonRpcRequest, + createDeferredPromise, } from '@metamask/utils'; -import { klona } from 'klona/full'; import { projectLogger, createModuleLogger } from './logging-utils'; -import type { JsonRpcRequestToCache } from './types'; import { cacheIdentifierForRequest } from './utils/cache'; -type RequestHandlers = (handledRes: PendingJsonRpcResponse) => void; +type RequestHandler = { + onSuccess: (result: Json) => void; + onError: (error: Error) => void; +}; type InflightRequest = { - [cacheId: string]: RequestHandlers[]; + [cacheId: string]: RequestHandler[]; }; -export type JsonRpcCacheMiddleware< - Params extends JsonRpcParams, - Result extends Json, -> = - JsonRpcMiddleware extends ( - req: JsonRpcRequest, - ...args: infer X - ) => infer Y - ? (req: JsonRpcRequestToCache, ...args: X) => Y - : never; - - const log = createModuleLogger(projectLogger, 'inflight-cache'); -export function createInflightCacheMiddleware(): JsonRpcCacheMiddleware< - JsonRpcParams, - Json +/** + * Creates a middleware that caches inflight requests. + * If a request is already in flight, the middleware will wait for the request to complete + * and then return the result. + * + * @returns A middleware that caches inflight requests. + */ +export function createInflightCacheMiddleware(): JsonRpcMiddleware< + JsonRpcRequest, + Json, + MiddlewareContext<{ skipCache: boolean }> > { const inflightRequests: InflightRequest = {}; - return createAsyncMiddleware( - async (req: JsonRpcRequestToCache, res, next) => { - // allow cach to be skipped if so specified - if (req.skipCache) { - return next(); - } - // get cacheId, if cacheable - const cacheId: string | null = cacheIdentifierForRequest(req); - // if not cacheable, skip - if (!cacheId) { - log('Request is not cacheable, proceeding. req = %o', req); - return next(); - } - // check for matching requests - let activeRequestHandlers: RequestHandlers[] = inflightRequests[cacheId]; - // if found, wait for the active request to be handled - if (activeRequestHandlers) { - // setup the response listener and wait for it to be called - // it will handle copying the result and request fields - log( - 'Running %i handler(s) for request %o', - activeRequestHandlers.length, - req, - ); - await createActiveRequestHandler(res, activeRequestHandlers); - return undefined; - } - // setup response handler array for subsequent requests - activeRequestHandlers = []; - inflightRequests[cacheId] = activeRequestHandlers; - // allow request to be handled normally - log('Carrying original request forward %o', req); - await next(); + return async ({ request, context, next }) => { + if (context.get('skipCache')) { + return next(); + } - // clear inflight requests - delete inflightRequests[cacheId]; - // schedule activeRequestHandlers to be handled + const cacheId: string | null = cacheIdentifierForRequest(request); + if (!cacheId) { + log('Request is not cacheable, proceeding. req = %o', request); + return next(); + } + + // check for matching requests + let activeRequestHandlers: RequestHandler[] = inflightRequests[cacheId]; + // if found, wait for the active request to be handled + if (activeRequestHandlers) { + // setup the response listener and wait for it to be called + // it will handle copying the result and request fields log( - 'Running %i collected handler(s) for request %o', + 'Running %i handler(s) for request %o', activeRequestHandlers.length, - req, + request, ); - handleActiveRequest(res, activeRequestHandlers); - // complete - return undefined; - }, - ); + return await createActiveRequestHandler(activeRequestHandlers); + } - async function createActiveRequestHandler( - res: PendingJsonRpcResponse, - activeRequestHandlers: RequestHandlers[], - ): Promise { - const { resolve, promise } = deferredPromise(); - activeRequestHandlers.push((handledRes: PendingJsonRpcResponse) => { - // append a copy of the result and error to the response - res.result = klona(handledRes.result); - res.error = klona(handledRes.error); - resolve(); + // setup response handler array for subsequent requests + activeRequestHandlers = []; + inflightRequests[cacheId] = activeRequestHandlers; + // allow request to be handled normally + log('Carrying original request forward %o', request); + try { + const result = (await next()) as Json; + log( + 'Running %i collected handler(s) for successful request %o', + activeRequestHandlers.length, + request, + ); + handleSuccess(result, activeRequestHandlers); + return result; + } catch (error) { + log( + 'Running %i collected handler(s) for failed request %o', + activeRequestHandlers.length, + request, + ); + handleError(error as Error, activeRequestHandlers); + throw error; + } finally { + delete inflightRequests[cacheId]; + } + }; + + /** + * Creates a new request handler for the active request. + * + * @param activeRequestHandlers - The active request handlers. + * @returns A promise that resolves to the result of the request. + */ + function createActiveRequestHandler( + activeRequestHandlers: RequestHandler[], + ): Promise { + const { resolve, promise, reject } = createDeferredPromise(); + activeRequestHandlers.push({ + onSuccess: (result: Json) => resolve(result), + onError: (error: Error) => reject(error), }); return promise; } - function handleActiveRequest( - res: PendingJsonRpcResponse, - activeRequestHandlers: RequestHandlers[], + /** + * Handles successful requests. + * + * @param result - The result of the request. + * @param activeRequestHandlers - The active request handlers. + */ + function handleSuccess( + result: Json, + activeRequestHandlers: RequestHandler[], ): void { // use setTimeout so we can resolve our original request first setTimeout(() => { - activeRequestHandlers.forEach((handler) => { - try { - handler(res); - } catch (err) { - // catch error so all requests are handled correctly - console.error(err); - } + activeRequestHandlers.forEach(({ onSuccess }) => { + onSuccess(result); }); }); } -} -function deferredPromise() { - let resolve: any; - const promise: Promise = new Promise((_resolve) => { - resolve = _resolve; - }); - return { resolve, promise }; + /** + * Handles failed requests. + * + * @param error - The error of the request. + * @param activeRequestHandlers - The active request handlers. + */ + function handleError( + error: Error, + activeRequestHandlers: RequestHandler[], + ): void { + activeRequestHandlers.forEach(({ onError }) => { + onError(error); + }); + } } diff --git a/packages/eth-json-rpc-middleware/src/types.ts b/packages/eth-json-rpc-middleware/src/types.ts index 17c4381ebb3..83d0c23ca2d 100644 --- a/packages/eth-json-rpc-middleware/src/types.ts +++ b/packages/eth-json-rpc-middleware/src/types.ts @@ -6,11 +6,6 @@ import type { JsonRpcResponse, } from '@metamask/utils'; -export type JsonRpcRequestToCache = - JsonRpcRequest & { - skipCache?: boolean; - }; - export type BlockData = string | string[]; export type Block = Record; From 17bd319cf544ee7d0d4a1ca364eb10b70b384d7b Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:25:17 -0700 Subject: [PATCH 07/30] feat: Add providerAsMiddlewareV2 --- eslint-warning-thresholds.json | 2 +- .../eth-json-rpc-middleware/src/index.test.ts | 1 + .../src/providerAsMiddleware.test.ts | 59 +++++++++++++++++++ .../src/providerAsMiddleware.ts | 13 +++- 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 3efe1f8c171..706f388c3d5 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -180,7 +180,7 @@ "jsdoc/match-description": 4 }, "packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts": { - "jsdoc/require-jsdoc": 1 + "jsdoc/require-jsdoc": 2 }, "packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts": { "jsdoc/require-jsdoc": 1 diff --git a/packages/eth-json-rpc-middleware/src/index.test.ts b/packages/eth-json-rpc-middleware/src/index.test.ts index 579aa41456d..fdb1730d485 100644 --- a/packages/eth-json-rpc-middleware/src/index.test.ts +++ b/packages/eth-json-rpc-middleware/src/index.test.ts @@ -13,6 +13,7 @@ describe('index module', () => { "createRetryOnEmptyMiddleware": [Function], "createWalletMiddleware": [Function], "providerAsMiddleware": [Function], + "providerAsMiddlewareV2": [Function], } `); }); diff --git a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts new file mode 100644 index 00000000000..615245d670c --- /dev/null +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts @@ -0,0 +1,59 @@ +import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; +import { assertIsJsonRpcSuccess, Json } from '@metamask/utils'; + +import { providerAsMiddleware, providerAsMiddlewareV2 } from './providerAsMiddleware'; +import { createRequest } from '../test/util/helpers'; + +const createMockProvider = (result: Json): SafeEventEmitterProvider => + ({ + request: jest.fn().mockResolvedValue(result), + } as unknown as SafeEventEmitterProvider); + +describe('providerAsMiddleware', () => { + it('forwards requests to the provider and returns the result', async () => { + const mockResult = 42; + const mockProvider = createMockProvider(mockResult); + + const engine = new JsonRpcEngine(); + engine.push(providerAsMiddleware(mockProvider)); + + const request = createRequest({ + method: 'eth_chainId', + params: [], + }); + + await new Promise((resolve) => { + engine.handle(request, (error, response) => { + expect(error).toBeNull(); + expect(response).toBeDefined(); + assertIsJsonRpcSuccess(response); + expect(response.result).toEqual(mockResult); + expect(mockProvider.request).toHaveBeenCalledWith(request); + resolve(); + }); + }); + }); +}); + +describe('providerAsMiddlewareV2', () => { + it('forwards requests to the provider and returns the result', async () => { + const mockResult = 123; + const mockProvider = createMockProvider(mockResult); + + const engine = JsonRpcEngineV2.create({ + middleware: [providerAsMiddlewareV2(mockProvider)], + }); + + const request = createRequest({ + method: 'eth_chainId', + params: [], + }); + + const result = await engine.handle(request); + + expect(result).toEqual(mockResult); + expect(mockProvider.request).toHaveBeenCalledWith(request); + }); +}); diff --git a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts index c070b191fae..8e5edab8f64 100644 --- a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts @@ -1,14 +1,21 @@ import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { createAsyncMiddleware, - type JsonRpcMiddleware, + type JsonRpcMiddleware as LegacyJsonRpcMiddleware, } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcParams } from '@metamask/utils'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; export function providerAsMiddleware( provider: InternalProvider, -): JsonRpcMiddleware { +): LegacyJsonRpcMiddleware { return createAsyncMiddleware(async (req, res) => { res.result = await provider.request(req); }); } + +export function providerAsMiddlewareV2( + provider: InternalProvider, +): JsonRpcMiddleware { + return async ({ request }) => provider.request(request); +} From beb94b424deef800595ac208d760f626168bfd4a Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:42:00 -0700 Subject: [PATCH 08/30] refactor: Rewrite retryOnEmpty --- .../src/retryOnEmpty.test.ts | 50 +++++++------------ .../src/retryOnEmpty.ts | 27 +++++----- .../test/util/helpers.ts | 12 ----- 3 files changed, 32 insertions(+), 57 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/retryOnEmpty.test.ts b/packages/eth-json-rpc-middleware/src/retryOnEmpty.test.ts index 6fbdaf4e00b..3cf930067a8 100644 --- a/packages/eth-json-rpc-middleware/src/retryOnEmpty.test.ts +++ b/packages/eth-json-rpc-middleware/src/retryOnEmpty.test.ts @@ -1,4 +1,4 @@ -import { errorCodes, providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { createRetryOnEmptyMiddleware } from '.'; @@ -6,7 +6,6 @@ import type { ProviderRequestStub } from '../test/util/helpers'; import { createMockParamsWithBlockParamAt, createMockParamsWithoutBlockParamAt, - createSimpleFinalMiddleware, createStubForBlockNumberRequest, expectProviderRequestNotToHaveBeenMade, requestMatches, @@ -14,6 +13,7 @@ import { createProviderAndBlockTracker, createEngine, createRequest, + createFinalMiddlewareWithDefaultResult, } from '../test/util/helpers'; const originalSetTimeout = globalThis.setTimeout; @@ -100,18 +100,14 @@ describe('createRetryOnEmptyMiddleware', () => { }), ]); - const responsePromise = engine.handle(request); + const resultPromise = engine.handle(request); await waitForRequestToBeRetried({ requestSpy, request, numberOfTimes: 10, }); - expect(await responsePromise).toStrictEqual({ - id: 1, - jsonrpc: '2.0', - result: 'something', - }); + expect(await resultPromise).toBe('something'); }); it('returns an error if the request is still unsuccessful after 10 retries', async () => { @@ -141,26 +137,20 @@ describe('createRetryOnEmptyMiddleware', () => { }), ]); - const responsePromise = engine.handle(request); + const resultPromise = engine.handle(request); await waitForRequestToBeRetried({ requestSpy, request, numberOfTimes: 10, }); - expect(await responsePromise).toMatchObject({ - error: expect.objectContaining({ - data: expect.objectContaining({ - cause: expect.objectContaining({ - message: 'RetryOnEmptyMiddleware - retries exhausted', - }), - }), - }), - }); + await expect(resultPromise).rejects.toThrow( + new Error('RetryOnEmptyMiddleware - retries exhausted'), + ); }); it('does not proceed to the next middleware after making a request through the provider', async () => { - const finalMiddleware = createSimpleFinalMiddleware(); + const finalMiddleware = createFinalMiddlewareWithDefaultResult(); const engine = createEngine( createRetryOnEmptyMiddleware({ @@ -219,7 +209,7 @@ describe('createRetryOnEmptyMiddleware', () => { }); it('proceeds to the next middleware', async () => { - const finalMiddleware = createSimpleFinalMiddleware(); + const finalMiddleware = createFinalMiddlewareWithDefaultResult(); const engine = createEngine( createRetryOnEmptyMiddleware({ @@ -276,7 +266,7 @@ describe('createRetryOnEmptyMiddleware', () => { }); it('proceeds to the next middleware', async () => { - const finalMiddleware = createSimpleFinalMiddleware(); + const finalMiddleware = createFinalMiddlewareWithDefaultResult(); const engine = createEngine( createRetryOnEmptyMiddleware({ @@ -334,7 +324,7 @@ describe('createRetryOnEmptyMiddleware', () => { }); it('proceeds to the next middleware', async () => { - const finalMiddleware = createSimpleFinalMiddleware(); + const finalMiddleware = createFinalMiddlewareWithDefaultResult(); const engine = createEngine( createRetryOnEmptyMiddleware({ @@ -389,7 +379,7 @@ describe('createRetryOnEmptyMiddleware', () => { }); it('proceeds to the next middleware', async () => { - const finalMiddleware = createSimpleFinalMiddleware(); + const finalMiddleware = createFinalMiddlewareWithDefaultResult(); const engine = createEngine( createRetryOnEmptyMiddleware({ @@ -437,7 +427,7 @@ describe('createRetryOnEmptyMiddleware', () => { }); it('proceeds to the next middleware', async () => { - const finalMiddleware = createSimpleFinalMiddleware(); + const finalMiddleware = createFinalMiddlewareWithDefaultResult(); const engine = createEngine( createRetryOnEmptyMiddleware({ @@ -478,13 +468,11 @@ describe('createRetryOnEmptyMiddleware', () => { }, }, ]); - const responsePromise = engine.handle(request); - expect(await responsePromise).toMatchObject({ - error: expect.objectContaining({ - code: errorCodes.rpc.invalidInput, - message: 'execution reverted', - }), - }); + + const resultPromise = engine.handle(request); + await expect(resultPromise).rejects.toThrow( + rpcErrors.invalidInput('execution reverted'), + ); }); }); }); diff --git a/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts b/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts index f9447a8dbc6..e227e3385cc 100644 --- a/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts +++ b/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts @@ -1,9 +1,8 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcParams } from '@metamask/utils'; -import { klona } from 'klona/full'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +import { klona } from 'klona'; import { projectLogger, createModuleLogger } from './logging-utils'; import type { Block } from './types'; @@ -37,7 +36,7 @@ export function createRetryOnEmptyMiddleware({ }: { provider?: InternalProvider; blockTracker?: PollingBlockTracker; -} = {}): JsonRpcMiddleware { +} = {}): JsonRpcMiddleware { if (!provider) { throw Error( 'RetryOnEmptyMiddleware - mandatory "provider" option is missing.', @@ -50,16 +49,18 @@ export function createRetryOnEmptyMiddleware({ ); } - return createAsyncMiddleware(async (req, res, next) => { - const blockRefIndex: number | undefined = blockTagParamIndex(req.method); + return async ({ request, next }) => { + const blockRefIndex: number | undefined = blockTagParamIndex( + request.method, + ); // skip if method does not include blockRef if (blockRefIndex === undefined) { return next(); } // skip if not exact block references let blockRef: string | undefined = - Array.isArray(req.params) && req.params[blockRefIndex] - ? (req.params[blockRefIndex] as string) + Array.isArray(request.params) && request.params[blockRefIndex] + ? (request.params[blockRefIndex] as string) : undefined; // omitted blockRef implies "latest" if (blockRef === undefined) { @@ -98,7 +99,7 @@ export function createRetryOnEmptyMiddleware({ ); // create child request with specific block-ref - const childRequest = klona(req); + const childRequest = klona(request); // attempt child request until non-empty response is received const childResult = await retry(10, async () => { log('Performing request %o', childRequest); @@ -118,10 +119,8 @@ export function createRetryOnEmptyMiddleware({ return attemptResult; }); log('Copying result %o', childResult); - // copy child result onto original response - res.result = childResult; - return undefined; - }); + return childResult; + }; } /** diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index f1a88ac5693..a1a691b779d 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -120,18 +120,6 @@ export function createEngine( }); } -/** - * Creates a middleware function that just ends the request, but is also a Jest - * mock function so that you can make assertions on it. - * - * @returns The created middleware, as a mock function. - */ -export function createSimpleFinalMiddleware() { - return jest.fn((_req, _res, _next, end) => { - end(); - }); -} - /** * Some JSON-RPC endpoints take a "block" param (example: `eth_blockNumber`) * which can optionally be left out. Additionally, the endpoint may support some From 1f1c4912b71d3eb6077c50993284024a6ff6bf23 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:09:05 -0700 Subject: [PATCH 09/30] refactor: Rewrite wallet --- eslint-warning-thresholds.json | 8 +- .../src/block-cache.ts | 2 +- .../src/block-ref.test.ts | 1 + ...llet-request-execution-permissions.test.ts | 18 +- .../wallet-request-execution-permissions.ts | 34 +- ...wallet-revoke-execution-permission.test.ts | 18 +- .../wallet-revoke-execution-permission.ts | 48 +-- .../src/providerAsMiddleware.test.ts | 14 +- packages/eth-json-rpc-middleware/src/types.ts | 1 - .../src/wallet.test.ts | 380 +++++++++--------- .../eth-json-rpc-middleware/src/wallet.ts | 274 ++++++------- 11 files changed, 387 insertions(+), 411 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 706f388c3d5..266558456ca 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -154,7 +154,6 @@ "@typescript-eslint/no-explicit-any": 1 }, "packages/eth-json-rpc-middleware/src/block-cache.ts": { - "@typescript-eslint/no-explicit-any": 1, "jsdoc/require-jsdoc": 1, "no-restricted-syntax": 1 }, @@ -180,13 +179,13 @@ "jsdoc/match-description": 4 }, "packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts": { - "jsdoc/require-jsdoc": 2 + "jsdoc/require-jsdoc": 1 }, "packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts": { "jsdoc/require-jsdoc": 1 }, "packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts": { - "jsdoc/require-jsdoc": 1 + "jsdoc/require-jsdoc": 2 }, "packages/eth-json-rpc-middleware/src/retryOnEmpty.test.ts": { "jsdoc/match-description": 3 @@ -213,7 +212,6 @@ "jsdoc/require-jsdoc": 4 }, "packages/eth-json-rpc-middleware/src/wallet.ts": { - "@typescript-eslint/no-explicit-any": 2, "@typescript-eslint/prefer-nullish-coalescing": 5, "jsdoc/match-description": 3, "jsdoc/require-jsdoc": 12 @@ -224,7 +222,7 @@ }, "packages/eth-json-rpc-middleware/test/util/helpers.ts": { "@typescript-eslint/no-explicit-any": 5, - "jsdoc/match-description": 11 + "jsdoc/match-description": 10 }, "packages/gas-fee-controller/src/GasFeeController.test.ts": { "import-x/namespace": 2, diff --git a/packages/eth-json-rpc-middleware/src/block-cache.ts b/packages/eth-json-rpc-middleware/src/block-cache.ts index cebc9e5d716..0c92d8c2a7b 100644 --- a/packages/eth-json-rpc-middleware/src/block-cache.ts +++ b/packages/eth-json-rpc-middleware/src/block-cache.ts @@ -3,7 +3,7 @@ import type { JsonRpcMiddleware, MiddlewareContext, } from '@metamask/json-rpc-engine/v2'; -import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; import { projectLogger, createModuleLogger } from './logging-utils'; import type { diff --git a/packages/eth-json-rpc-middleware/src/block-ref.test.ts b/packages/eth-json-rpc-middleware/src/block-ref.test.ts index 45d215abef4..ef296ecb9a2 100644 --- a/packages/eth-json-rpc-middleware/src/block-ref.test.ts +++ b/packages/eth-json-rpc-middleware/src/block-ref.test.ts @@ -1,4 +1,5 @@ import { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; + import { createBlockRefMiddleware } from '.'; import { createMockParamsWithBlockParamAt, diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts index cfd73c7d7a7..bcbb8d19009 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts @@ -1,4 +1,5 @@ -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { MiddlewareParams } from '@metamask/json-rpc-engine/v2'; +import type { JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona'; import type { @@ -6,7 +7,7 @@ import type { RequestExecutionPermissionsRequestParams, RequestExecutionPermissionsResult, } from './wallet-request-execution-permissions'; -import { walletRequestExecutionPermissions } from './wallet-request-execution-permissions'; +import { createWalletRequestExecutionPermissionsHandler } from './wallet-request-execution-permissions'; const ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123a'; const CHAIN_ID_MOCK = '0x1'; @@ -66,14 +67,14 @@ const RESULT_MOCK: RequestExecutionPermissionsResult = [ describe('wallet_requestExecutionPermissions', () => { let request: JsonRpcRequest; let params: RequestExecutionPermissionsRequestParams; - let response: PendingJsonRpcResponse; let processRequestExecutionPermissionsMock: jest.MockedFunction; const callMethod = async () => { - return walletRequestExecutionPermissions(request, response, { + const handler = createWalletRequestExecutionPermissionsHandler({ processRequestExecutionPermissions: processRequestExecutionPermissionsMock, }); + return handler({ request } as MiddlewareParams); }; beforeEach(() => { @@ -81,7 +82,6 @@ describe('wallet_requestExecutionPermissions', () => { request = klona(REQUEST_MOCK); params = request.params as RequestExecutionPermissionsRequestParams; - response = {} as PendingJsonRpcResponse; processRequestExecutionPermissionsMock = jest.fn(); processRequestExecutionPermissionsMock.mockResolvedValue(RESULT_MOCK); @@ -96,8 +96,8 @@ describe('wallet_requestExecutionPermissions', () => { }); it('returns result from hook', async () => { - await callMethod(); - expect(response.result).toStrictEqual(RESULT_MOCK); + const result = await callMethod(); + expect(result).toStrictEqual(RESULT_MOCK); }); it('supports null rules', async () => { @@ -124,7 +124,9 @@ describe('wallet_requestExecutionPermissions', () => { it('throws if no hook', async () => { await expect( - walletRequestExecutionPermissions(request, response, {}), + createWalletRequestExecutionPermissionsHandler({})({ + request, + } as MiddlewareParams), ).rejects.toThrow( `wallet_requestExecutionPermissions - no middleware configured`, ); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts index 62fdf2bb738..7291a56e184 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts @@ -1,3 +1,4 @@ +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import { rpcErrors } from '@metamask/rpc-errors'; import type { Infer } from '@metamask/superstruct'; import { @@ -16,7 +17,6 @@ import { type Hex, type Json, type JsonRpcRequest, - type PendingJsonRpcResponse, StrictHexStruct, } from '@metamask/utils'; @@ -66,24 +66,22 @@ export type ProcessRequestExecutionPermissionsHook = ( req: JsonRpcRequest, ) => Promise; -export async function walletRequestExecutionPermissions( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - { - processRequestExecutionPermissions, - }: { - processRequestExecutionPermissions?: ProcessRequestExecutionPermissionsHook; - }, -): Promise { - if (!processRequestExecutionPermissions) { - throw rpcErrors.methodNotSupported( - 'wallet_requestExecutionPermissions - no middleware configured', - ); - } +export function createWalletRequestExecutionPermissionsHandler({ + processRequestExecutionPermissions, +}: { + processRequestExecutionPermissions?: ProcessRequestExecutionPermissionsHook; +}): JsonRpcMiddleware { + return async ({ request }) => { + if (!processRequestExecutionPermissions) { + throw rpcErrors.methodNotSupported( + 'wallet_requestExecutionPermissions - no middleware configured', + ); + } - const { params } = req; + const { params } = request; - validateParams(params, RequestExecutionPermissionsStruct); + validateParams(params, RequestExecutionPermissionsStruct); - res.result = await processRequestExecutionPermissions(params, req); + return await processRequestExecutionPermissions(params, request); + }; } diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts index 3bbfdb8868a..ba087f27599 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts @@ -1,11 +1,12 @@ -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { MiddlewareParams } from '@metamask/json-rpc-engine/v2'; +import type { JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona'; import type { ProcessRevokeExecutionPermissionHook, RevokeExecutionPermissionRequestParams, } from './wallet-revoke-execution-permission'; -import { walletRevokeExecutionPermission } from './wallet-revoke-execution-permission'; +import { createWalletRevokeExecutionPermissionHandler } from './wallet-revoke-execution-permission'; const HEX_MOCK = '0x123abc'; @@ -18,13 +19,13 @@ const REQUEST_MOCK = { describe('wallet_revokeExecutionPermission', () => { let request: JsonRpcRequest; let params: RevokeExecutionPermissionRequestParams; - let response: PendingJsonRpcResponse; let processRevokeExecutionPermissionMock: jest.MockedFunction; const callMethod = async () => { - return walletRevokeExecutionPermission(request, response, { + const handler = createWalletRevokeExecutionPermissionHandler({ processRevokeExecutionPermission: processRevokeExecutionPermissionMock, }); + return handler({ request } as MiddlewareParams); }; beforeEach(() => { @@ -32,7 +33,6 @@ describe('wallet_revokeExecutionPermission', () => { request = klona(REQUEST_MOCK); params = request.params as RevokeExecutionPermissionRequestParams; - response = {} as PendingJsonRpcResponse; processRevokeExecutionPermissionMock = jest.fn(); processRevokeExecutionPermissionMock.mockResolvedValue({}); @@ -47,13 +47,15 @@ describe('wallet_revokeExecutionPermission', () => { }); it('returns result from hook', async () => { - await callMethod(); - expect(response.result).toStrictEqual({}); + const result = await callMethod(); + expect(result).toStrictEqual({}); }); it('throws if no hook', async () => { await expect( - walletRevokeExecutionPermission(request, response, {}), + createWalletRevokeExecutionPermissionHandler({})({ + request, + } as MiddlewareParams), ).rejects.toThrow( 'wallet_revokeExecutionPermission - no middleware configured', ); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts index b4343073a5c..a9e6069e742 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts @@ -1,11 +1,9 @@ +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import { rpcErrors } from '@metamask/rpc-errors'; import type { Infer } from '@metamask/superstruct'; -import { - type JsonRpcRequest, - object, - type PendingJsonRpcResponse, - StrictHexStruct, -} from '@metamask/utils'; +import { object } from '@metamask/superstruct'; +import type { Json } from '@metamask/utils'; +import { type JsonRpcRequest, StrictHexStruct } from '@metamask/utils'; import { validateParams } from '../utils/validation'; @@ -28,24 +26,22 @@ export type ProcessRevokeExecutionPermissionHook = ( req: JsonRpcRequest, ) => Promise; -export async function walletRevokeExecutionPermission( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - { - processRevokeExecutionPermission, - }: { - processRevokeExecutionPermission?: ProcessRevokeExecutionPermissionHook; - }, -): Promise { - if (!processRevokeExecutionPermission) { - throw rpcErrors.methodNotSupported( - 'wallet_revokeExecutionPermission - no middleware configured', - ); - } - - const { params } = req; - - validateParams(params, RevokeExecutionPermissionRequestParamsStruct); - - res.result = await processRevokeExecutionPermission(params, req); +export function createWalletRevokeExecutionPermissionHandler({ + processRevokeExecutionPermission, +}: { + processRevokeExecutionPermission?: ProcessRevokeExecutionPermissionHook; +}): JsonRpcMiddleware { + return async ({ request }) => { + if (!processRevokeExecutionPermission) { + throw rpcErrors.methodNotSupported( + 'wallet_revokeExecutionPermission - no middleware configured', + ); + } + + const { params } = request; + + validateParams(params, RevokeExecutionPermissionRequestParamsStruct); + + return await processRevokeExecutionPermission(params, request); + }; } diff --git a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts index 615245d670c..d404b5c4b19 100644 --- a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts @@ -1,15 +1,19 @@ import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; -import { assertIsJsonRpcSuccess, Json } from '@metamask/utils'; +import type { Json } from '@metamask/utils'; +import { assertIsJsonRpcSuccess } from '@metamask/utils'; -import { providerAsMiddleware, providerAsMiddlewareV2 } from './providerAsMiddleware'; +import { + providerAsMiddleware, + providerAsMiddlewareV2, +} from './providerAsMiddleware'; import { createRequest } from '../test/util/helpers'; const createMockProvider = (result: Json): SafeEventEmitterProvider => ({ request: jest.fn().mockResolvedValue(result), - } as unknown as SafeEventEmitterProvider); + }) as unknown as SafeEventEmitterProvider; describe('providerAsMiddleware', () => { it('forwards requests to the provider and returns the result', async () => { @@ -29,7 +33,7 @@ describe('providerAsMiddleware', () => { expect(error).toBeNull(); expect(response).toBeDefined(); assertIsJsonRpcSuccess(response); - expect(response.result).toEqual(mockResult); + expect(response.result).toStrictEqual(mockResult); expect(mockProvider.request).toHaveBeenCalledWith(request); resolve(); }); @@ -53,7 +57,7 @@ describe('providerAsMiddlewareV2', () => { const result = await engine.handle(request); - expect(result).toEqual(mockResult); + expect(result).toStrictEqual(mockResult); expect(mockProvider.request).toHaveBeenCalledWith(request); }); }); diff --git a/packages/eth-json-rpc-middleware/src/types.ts b/packages/eth-json-rpc-middleware/src/types.ts index 83d0c23ca2d..2c992c8720c 100644 --- a/packages/eth-json-rpc-middleware/src/types.ts +++ b/packages/eth-json-rpc-middleware/src/types.ts @@ -1,4 +1,3 @@ -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; import type { Json, JsonRpcParams, diff --git a/packages/eth-json-rpc-middleware/src/wallet.test.ts b/packages/eth-json-rpc-middleware/src/wallet.test.ts index a92f1407d40..7e235027727 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.test.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.test.ts @@ -1,5 +1,4 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import pify from 'pify'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import type { MessageParams, @@ -8,6 +7,7 @@ import type { TypedMessageV1Params, } from '.'; import { createWalletMiddleware } from '.'; +import { createRequest } from '../test/util/helpers'; const testAddresses = [ '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', @@ -22,53 +22,64 @@ const testMsgSig = describe('wallet', () => { describe('accounts', () => { it('returns null for coinbase when no accounts', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => []; - engine.push(createWalletMiddleware({ getAccounts })); - const coinbaseResult = await pify(engine.handle).call(engine, { - method: 'eth_coinbase', + const engine = JsonRpcEngineV2.create({ + middleware: [createWalletMiddleware({ getAccounts })], }); - expect(coinbaseResult.result).toBeNull(); + const coinbaseResult = await engine.handle( + createRequest({ + method: 'eth_coinbase', + }), + ); + expect(coinbaseResult).toBeNull(); }); it('should return the correct value from getAccounts', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); - engine.push(createWalletMiddleware({ getAccounts })); - const coinbaseResult = await pify(engine.handle).call(engine, { - method: 'eth_coinbase', + const engine = JsonRpcEngineV2.create({ + middleware: [createWalletMiddleware({ getAccounts })], }); - expect(coinbaseResult.result).toStrictEqual(testAddresses[0]); + const coinbaseResult = await engine.handle( + createRequest({ + method: 'eth_coinbase', + }), + ); + expect(coinbaseResult).toStrictEqual(testAddresses[0]); }); it('should return the correct value from getAccounts with multiple accounts', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); - engine.push(createWalletMiddleware({ getAccounts })); - const coinbaseResult = await pify(engine.handle).call(engine, { - method: 'eth_coinbase', + const engine = JsonRpcEngineV2.create({ + middleware: [createWalletMiddleware({ getAccounts })], }); - expect(coinbaseResult.result).toStrictEqual(testAddresses[0]); + const coinbaseResult = await engine.handle( + createRequest({ + method: 'eth_coinbase', + }), + ); + expect(coinbaseResult).toStrictEqual(testAddresses[0]); }); }); describe('transactions', () => { it('processes transaction with valid address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); const witnessedTxParams: TransactionParams[] = []; const processTransaction = async (_txParams: TransactionParams) => { witnessedTxParams.push(_txParams); return testTxHash; }; - engine.push(createWalletMiddleware({ getAccounts, processTransaction })); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTransaction }), + ], + }); const txParams = { from: testAddresses[0], }; const payload = { method: 'eth_sendTransaction', params: [txParams] }; - const sendTxResponse = await pify(engine.handle).call(engine, payload); - const sendTxResult = sendTxResponse.result; + const sendTxResult = await engine.handle(createRequest(payload)); expect(sendTxResult).toBeDefined(); expect(sendTxResult).toStrictEqual(testTxHash); expect(witnessedTxParams).toHaveLength(1); @@ -76,60 +87,78 @@ describe('wallet', () => { }); it('throws when provided an invalid address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); const witnessedTxParams: TransactionParams[] = []; const processTransaction = async (_txParams: TransactionParams) => { witnessedTxParams.push(_txParams); return testTxHash; }; - engine.push(createWalletMiddleware({ getAccounts, processTransaction })); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTransaction }), + ], + }); const txParams = { from: '0x3d', }; - const payload = { method: 'eth_sendTransaction', params: [txParams] }; - await expect(pify(engine.handle).call(engine, payload)).rejects.toThrow( + const payload = createRequest({ + method: 'eth_sendTransaction', + params: [txParams], + }); + await expect(engine.handle(payload)).rejects.toThrow( new Error('Invalid parameters: must provide an Ethereum address.'), ); }); it('throws unauthorized for unknown addresses', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); const witnessedTxParams: TransactionParams[] = []; const processTransaction = async (_txParams: TransactionParams) => { witnessedTxParams.push(_txParams); return testTxHash; }; - engine.push(createWalletMiddleware({ getAccounts, processTransaction })); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTransaction }), + ], + }); const txParams = { from: testUnkownAddress, }; - const payload = { method: 'eth_sendTransaction', params: [txParams] }; - const promise = pify(engine.handle).call(engine, payload); + const payload = createRequest({ + method: 'eth_sendTransaction', + params: [txParams], + }); + const promise = engine.handle(payload); await expect(promise).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); it('should not override other request params', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); const witnessedTxParams: TransactionParams[] = []; const processTransaction = async (_txParams: TransactionParams) => { witnessedTxParams.push(_txParams); return testTxHash; }; - engine.push(createWalletMiddleware({ getAccounts, processTransaction })); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTransaction }), + ], + }); const txParams = { from: testAddresses[0], to: testAddresses[1], }; - const payload = { method: 'eth_sendTransaction', params: [txParams] }; - await pify(engine.handle).call(engine, payload); + const payload = createRequest({ + method: 'eth_sendTransaction', + params: [txParams], + }); + await engine.handle(payload); expect(witnessedTxParams).toHaveLength(1); expect(witnessedTxParams[0]).toStrictEqual(txParams); }); @@ -137,24 +166,23 @@ describe('wallet', () => { describe('signTransaction', () => { it('should process sign transaction when provided a valid address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); const witnessedTxParams: TransactionParams[] = []; const processSignTransaction = async (_txParams: TransactionParams) => { witnessedTxParams.push(_txParams); return testTxHash; }; - - engine.push( - createWalletMiddleware({ getAccounts, processSignTransaction }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processSignTransaction }), + ], + }); const txParams = { from: testAddresses[0], }; const payload = { method: 'eth_signTransaction', params: [txParams] }; - const sendTxResponse = await pify(engine.handle).call(engine, payload); - const sendTxResult = sendTxResponse.result; + const sendTxResult = await engine.handle(createRequest(payload)); expect(sendTxResult).toBeDefined(); expect(sendTxResult).toStrictEqual(testTxHash); expect(witnessedTxParams).toHaveLength(1); @@ -162,68 +190,68 @@ describe('wallet', () => { }); it('should not override other request params', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); const witnessedTxParams: TransactionParams[] = []; const processSignTransaction = async (_txParams: TransactionParams) => { witnessedTxParams.push(_txParams); return testTxHash; }; - - engine.push( - createWalletMiddleware({ getAccounts, processSignTransaction }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processSignTransaction }), + ], + }); const txParams = { from: testAddresses[0], to: testAddresses[1], }; const payload = { method: 'eth_signTransaction', params: [txParams] }; - await pify(engine.handle).call(engine, payload); + await engine.handle(createRequest(payload)); expect(witnessedTxParams).toHaveLength(1); expect(witnessedTxParams[0]).toStrictEqual(txParams); }); it('should throw when provided invalid address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); const witnessedTxParams: TransactionParams[] = []; const processSignTransaction = async (_txParams: TransactionParams) => { witnessedTxParams.push(_txParams); return testTxHash; }; - - engine.push( - createWalletMiddleware({ getAccounts, processSignTransaction }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processSignTransaction }), + ], + }); const txParams = { from: '0x3', }; const payload = { method: 'eth_signTransaction', params: [txParams] }; - await expect(pify(engine.handle).call(engine, payload)).rejects.toThrow( + await expect(engine.handle(createRequest(payload))).rejects.toThrow( new Error('Invalid parameters: must provide an Ethereum address.'), ); }); it('should throw when provided unknown address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(0, 2); const witnessedTxParams: TransactionParams[] = []; const processSignTransaction = async (_txParams: TransactionParams) => { witnessedTxParams.push(_txParams); return testTxHash; }; - - engine.push( - createWalletMiddleware({ getAccounts, processSignTransaction }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processSignTransaction }), + ], + }); const txParams = { from: testUnkownAddress, }; const payload = { method: 'eth_signTransaction', params: [txParams] }; - const promise = pify(engine.handle).call(engine, payload); + const promise = engine.handle(createRequest(payload)); await expect(promise).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); @@ -232,15 +260,17 @@ describe('wallet', () => { describe('signTypedData', () => { it('should sign with a valid address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageV1Params[] = []; const processTypedMessage = async (msgParams: TypedMessageV1Params) => { witnessedMsgParams.push(msgParams); return testMsgSig; }; - - engine.push(createWalletMiddleware({ getAccounts, processTypedMessage })); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessage }), + ], + }); const message = [ { type: 'string', @@ -253,8 +283,7 @@ describe('wallet', () => { method: 'eth_signTypedData', params: [message, testAddresses[0]], }; - const signMsgResponse = await pify(engine.handle).call(engine, payload); - const signMsgResult = signMsgResponse.result; + const signMsgResult = await engine.handle(createRequest(payload)); expect(signMsgResult).toBeDefined(); expect(signMsgResult).toStrictEqual(testMsgSig); @@ -268,15 +297,17 @@ describe('wallet', () => { }); it('should throw with invalid address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageV1Params[] = []; const processTypedMessage = async (msgParams: TypedMessageV1Params) => { witnessedMsgParams.push(msgParams); return testMsgSig; }; - - engine.push(createWalletMiddleware({ getAccounts, processTypedMessage })); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessage }), + ], + }); const message = [ { type: 'string', @@ -289,21 +320,23 @@ describe('wallet', () => { method: 'eth_signTypedData', params: [message, '0x3d'], }; - await expect(pify(engine.handle).call(engine, payload)).rejects.toThrow( + await expect(engine.handle(createRequest(payload))).rejects.toThrow( new Error('Invalid parameters: must provide an Ethereum address.'), ); }); it('should throw with unknown address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageV1Params[] = []; const processTypedMessage = async (msgParams: TypedMessageV1Params) => { witnessedMsgParams.push(msgParams); return testMsgSig; }; - - engine.push(createWalletMiddleware({ getAccounts, processTypedMessage })); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessage }), + ], + }); const message = [ { type: 'string', @@ -316,7 +349,7 @@ describe('wallet', () => { method: 'eth_signTypedData', params: [message, testUnkownAddress], }; - const promise = pify(engine.handle).call(engine, payload); + const promise = engine.handle(createRequest(payload)); await expect(promise).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); @@ -325,7 +358,6 @@ describe('wallet', () => { describe('signTypedDataV3', () => { it('should sign data and normalizes verifyingContract', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV3 = async (msgParams: TypedMessageParams) => { @@ -333,10 +365,11 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV3 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV3 }), + ], + }); const message = { types: { @@ -367,11 +400,7 @@ describe('wallet', () => { params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup }; - const signTypedDataV3Response = await pify(engine.handle).call( - engine, - payload, - ); - const signTypedDataV3Result = signTypedDataV3Response.result; + const signTypedDataV3Result = await engine.handle(createRequest(payload)); expect(signTypedDataV3Result).toBeDefined(); expect(signTypedDataV3Result).toStrictEqual(testMsgSig); @@ -385,7 +414,6 @@ describe('wallet', () => { }); it('should throw if verifyingContract is invalid hex value', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV3 = async (msgParams: TypedMessageParams) => { @@ -393,10 +421,11 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV3 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV3 }), + ], + }); const message = { types: { @@ -421,12 +450,11 @@ describe('wallet', () => { params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup }; - const promise = pify(engine.handle).call(engine, payload); + const promise = engine.handle(createRequest(payload)); await expect(promise).rejects.toThrow('Invalid input.'); }); it('should not throw if verifyingContract is undefined', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV3 = async (msgParams: TypedMessageParams) => { @@ -434,10 +462,11 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV3 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV3 }), + ], + }); const message = { types: { @@ -459,14 +488,10 @@ describe('wallet', () => { params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup }; - const promise = pify(engine.handle).call(engine, payload); - const result = await promise; - expect(result).toStrictEqual({ - id: undefined, - jsonrpc: undefined, - result: - '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', - }); + const result = await engine.handle(createRequest(payload)); + expect(result).toBe( + '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', + ); }); }); @@ -505,7 +530,6 @@ describe('wallet', () => { }); it('should not throw if request is permit with valid hex value for verifyingContract address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV4 = async (msgParams: TypedMessageParams) => { @@ -513,28 +537,24 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV4 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV4 }), + ], + }); const payload = { method: 'eth_signTypedData_v4', params: [testAddresses[0], JSON.stringify(getMsgParams())], }; - const promise = pify(engine.handle).call(engine, payload); - const result = await promise; - expect(result).toStrictEqual({ - id: undefined, - jsonrpc: undefined, - result: - '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', - }); + const result = await engine.handle(createRequest(payload)); + expect(result).toBe( + '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', + ); }); it('should throw if request is permit with invalid hex value for verifyingContract address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV4 = async (msgParams: TypedMessageParams) => { @@ -542,10 +562,11 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV4 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV4 }), + ], + }); const payload = { method: 'eth_signTypedData_v4', @@ -557,12 +578,11 @@ describe('wallet', () => { ], }; - const promise = pify(engine.handle).call(engine, payload); + const promise = engine.handle(createRequest(payload)); await expect(promise).rejects.toThrow('Invalid input.'); }); it('should not throw if request is permit with undefined value for verifyingContract address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV4 = async (msgParams: TypedMessageParams) => { @@ -570,28 +590,24 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV4 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV4 }), + ], + }); const payload = { method: 'eth_signTypedData_v4', params: [testAddresses[0], JSON.stringify(getMsgParams())], }; - const promise = pify(engine.handle).call(engine, payload); - const result = await promise; - expect(result).toStrictEqual({ - id: undefined, - jsonrpc: undefined, - result: - '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', - }); + const result = await engine.handle(createRequest(payload)); + expect(result).toBe( + '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', + ); }); it('should not throw if request is permit with verifyingContract address equal to "cosmos"', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV4 = async (msgParams: TypedMessageParams) => { @@ -599,28 +615,24 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV4 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV4 }), + ], + }); const payload = { method: 'eth_signTypedData_v4', params: [testAddresses[0], JSON.stringify(getMsgParams('cosmos'))], }; - const promise = pify(engine.handle).call(engine, payload); - const result = await promise; - expect(result).toStrictEqual({ - id: undefined, - jsonrpc: undefined, - result: - '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', - }); + const result = await engine.handle(createRequest(payload)); + expect(result).toBe( + '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', + ); }); it('should throw if message does not have types defined', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV4 = async (msgParams: TypedMessageParams) => { @@ -628,10 +640,11 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV4 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV4 }), + ], + }); const messageParams = getMsgParams(); const payload = { @@ -642,12 +655,11 @@ describe('wallet', () => { ], }; - const promise = pify(engine.handle).call(engine, payload); + const promise = engine.handle(createRequest(payload)); await expect(promise).rejects.toThrow('Invalid input.'); }); it('should throw if type of primaryType is not defined', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV4 = async (msgParams: TypedMessageParams) => { @@ -655,10 +667,11 @@ describe('wallet', () => { // Assume testMsgSig is the expected signature result return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processTypedMessageV4 }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV4 }), + ], + }); const messageParams = getMsgParams(); const payload = { @@ -672,32 +685,31 @@ describe('wallet', () => { ], }; - const promise = pify(engine.handle).call(engine, payload); + const promise = engine.handle(createRequest(payload)); await expect(promise).rejects.toThrow('Invalid input.'); }); }); describe('sign', () => { it('should sign with a valid address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: MessageParams[] = []; const processPersonalMessage = async (msgParams: MessageParams) => { witnessedMsgParams.push(msgParams); return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processPersonalMessage }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processPersonalMessage }), + ], + }); const message = 'haay wuurl'; const payload = { method: 'personal_sign', params: [message, testAddresses[0]], }; - const signMsgResponse = await pify(engine.handle).call(engine, payload); - const signMsgResult = signMsgResponse.result; + const signMsgResult = await engine.handle(createRequest(payload)); expect(signMsgResult).toBeDefined(); expect(signMsgResult).toStrictEqual(testMsgSig); @@ -710,17 +722,17 @@ describe('wallet', () => { }); it('should error when provided invalid address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: MessageParams[] = []; const processPersonalMessage = async (msgParams: MessageParams) => { witnessedMsgParams.push(msgParams); return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processPersonalMessage }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processPersonalMessage }), + ], + }); const message = 'haay wuurl'; const payload = { @@ -728,23 +740,23 @@ describe('wallet', () => { params: [message, '0x3d'], }; - await expect(pify(engine.handle).call(engine, payload)).rejects.toThrow( + await expect(engine.handle(createRequest(payload))).rejects.toThrow( new Error('Invalid parameters: must provide an Ethereum address.'), ); }); it('should error when provided unknown address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: MessageParams[] = []; const processPersonalMessage = async (msgParams: MessageParams) => { witnessedMsgParams.push(msgParams); return testMsgSig; }; - - engine.push( - createWalletMiddleware({ getAccounts, processPersonalMessage }), - ); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processPersonalMessage }), + ], + }); const message = 'haay wuurl'; const payload = { @@ -752,7 +764,7 @@ describe('wallet', () => { params: [message, testUnkownAddress], }; - const promise = pify(engine.handle).call(engine, payload); + const promise = engine.handle(createRequest(payload)); await expect(promise).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); @@ -771,15 +783,15 @@ describe('wallet', () => { addressHex: '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', }; - const engine = new JsonRpcEngine(); - engine.push(createWalletMiddleware({ getAccounts })); + const engine = JsonRpcEngineV2.create({ + middleware: [createWalletMiddleware({ getAccounts })], + }); const payload = { method: 'personal_ecRecover', params: [signParams.message, signParams.signature], }; - const ecrecoverResponse = await pify(engine.handle).call(engine, payload); - const ecrecoverResult = ecrecoverResponse.result; + const ecrecoverResult = await engine.handle(createRequest(payload)); expect(ecrecoverResult).toBeDefined(); expect(ecrecoverResult).toStrictEqual(signParams.addressHex); }); @@ -797,15 +809,15 @@ describe('wallet', () => { addressHex: '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', }; - const engine = new JsonRpcEngine(); - engine.push(createWalletMiddleware({ getAccounts })); + const engine = JsonRpcEngineV2.create({ + middleware: [createWalletMiddleware({ getAccounts })], + }); const payload = { method: 'personal_ecRecover', params: [signParams.message, signParams.signature], }; - const ecrecoverResponse = await pify(engine.handle).call(engine, payload); - const ecrecoverResult = ecrecoverResponse.result; + const ecrecoverResult = await engine.handle(createRequest(payload)); expect(ecrecoverResult).toBeDefined(); expect(ecrecoverResult).toStrictEqual(signParams.addressHex); }); diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index 944afe9e0e9..b6e129f648d 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -1,27 +1,21 @@ import * as sigUtil from '@metamask/eth-sig-util'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { - createAsyncMiddleware, - createScaffoldMiddleware, -} from '@metamask/json-rpc-engine'; +import type { + JsonRpcMiddleware, + MiddlewareParams, +} from '@metamask/json-rpc-engine/v2'; +import { createScaffoldMiddleware } from '@metamask/json-rpc-engine/v2'; import { rpcErrors } from '@metamask/rpc-errors'; import { isValidHexAddress } from '@metamask/utils'; -import type { - JsonRpcRequest, - PendingJsonRpcResponse, - Json, - Hex, -} from '@metamask/utils'; +import type { JsonRpcRequest, Json, Hex } from '@metamask/utils'; import { + createWalletRequestExecutionPermissionsHandler, type ProcessRequestExecutionPermissionsHook, - walletRequestExecutionPermissions, } from './methods/wallet-request-execution-permissions'; import { type ProcessRevokeExecutionPermissionHook, - walletRevokeExecutionPermission, + createWalletRevokeExecutionPermissionHandler, } from './methods/wallet-revoke-execution-permission'; -import type { Block } from './types'; import { stripArrayTypeIfPresent } from './utils/common'; import { normalizeTypedMessage, parseTypedMessage } from './utils/normalize'; import { @@ -29,19 +23,6 @@ import { validateAndNormalizeKeyholder as validateKeyholder, } from './utils/validation'; -/* -export type TransactionParams = { - [prop: string]: Json; - from: string; -} -*/ - -/* -export type TransactionParams = JsonRpcParams & { - from: string; -} -*/ - export type TransactionParams = { from: string; }; @@ -112,138 +93,130 @@ export function createWalletMiddleware({ processTypedMessageV4, processRequestExecutionPermissions, processRevokeExecutionPermission, -}: // }: WalletMiddlewareOptions): JsonRpcMiddleware { -WalletMiddlewareOptions): JsonRpcMiddleware { +}: WalletMiddlewareOptions): JsonRpcMiddleware { if (!getAccounts) { throw new Error('opts.getAccounts is required'); } return createScaffoldMiddleware({ // account lookups - eth_accounts: createAsyncMiddleware(lookupAccounts), - eth_coinbase: createAsyncMiddleware(lookupDefaultAccount), + eth_accounts: lookupAccounts, + eth_coinbase: lookupDefaultAccount, // tx signatures - eth_sendTransaction: createAsyncMiddleware(sendTransaction), - eth_signTransaction: createAsyncMiddleware(signTransaction), + eth_sendTransaction: sendTransaction, + eth_signTransaction: signTransaction, // message signatures - eth_signTypedData: createAsyncMiddleware(signTypedData), - eth_signTypedData_v3: createAsyncMiddleware(signTypedDataV3), - eth_signTypedData_v4: createAsyncMiddleware(signTypedDataV4), - personal_sign: createAsyncMiddleware(personalSign), - eth_getEncryptionPublicKey: createAsyncMiddleware(encryptionPublicKey), - eth_decrypt: createAsyncMiddleware(decryptMessage), - personal_ecRecover: createAsyncMiddleware(personalRecover), + eth_signTypedData: signTypedData, + eth_signTypedData_v3: signTypedDataV3, + eth_signTypedData_v4: signTypedDataV4, + personal_sign: personalSign, + eth_getEncryptionPublicKey: encryptionPublicKey, + eth_decrypt: decryptMessage, + personal_ecRecover: personalRecover, // EIP-7715 - wallet_requestExecutionPermissions: createAsyncMiddleware( - async (req, res) => - walletRequestExecutionPermissions(req, res, { - processRequestExecutionPermissions, - }), - ), - wallet_revokeExecutionPermission: createAsyncMiddleware(async (req, res) => - walletRevokeExecutionPermission(req, res, { + wallet_requestExecutionPermissions: + createWalletRequestExecutionPermissionsHandler({ + processRequestExecutionPermissions, + }), + wallet_revokeExecutionPermission: + createWalletRevokeExecutionPermissionHandler({ processRevokeExecutionPermission, }), - ), }); // // account lookups // - async function lookupAccounts( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { - res.result = await getAccounts(req); + async function lookupAccounts({ + request, + }: MiddlewareParams): Promise { + return await getAccounts(request); } - async function lookupDefaultAccount( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { - const accounts = await getAccounts(req); - res.result = accounts[0] || null; + async function lookupDefaultAccount({ + request, + }: MiddlewareParams): Promise { + const accounts = await getAccounts(request); + return accounts[0] || null; } // // transaction signatures // - async function sendTransaction( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + async function sendTransaction({ + request, + }: MiddlewareParams): Promise { if (!processTransaction) { throw rpcErrors.methodNotSupported(); } if ( - !req.params || - !Array.isArray(req.params) || - !(req.params.length >= 1) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 1) ) { throw rpcErrors.invalidInput(); } - const params = req.params[0] as TransactionParams | undefined; + const params = request.params[0] as TransactionParams | undefined; const txParams: TransactionParams = { ...params, - from: await validateAndNormalizeKeyholder(params?.from || '', req), + from: await validateAndNormalizeKeyholder(params?.from || '', request), }; - res.result = await processTransaction(txParams, req); + return await processTransaction(txParams, request); } - async function signTransaction( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + async function signTransaction({ + request, + }: MiddlewareParams): Promise { if (!processSignTransaction) { throw rpcErrors.methodNotSupported(); } if ( - !req.params || - !Array.isArray(req.params) || - !(req.params.length >= 1) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 1) ) { throw rpcErrors.invalidInput(); } - const params = req.params[0] as TransactionParams | undefined; + const params = request.params[0] as TransactionParams | undefined; const txParams: TransactionParams = { ...params, - from: await validateAndNormalizeKeyholder(params?.from || '', req), + from: await validateAndNormalizeKeyholder(params?.from || '', request), }; - res.result = await processSignTransaction(txParams, req); + return await processSignTransaction(txParams, request); } // // message signatures // - async function signTypedData( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + + async function signTypedData({ + request, + }: MiddlewareParams): Promise { if (!processTypedMessage) { throw rpcErrors.methodNotSupported(); } if ( - !req?.params || - !Array.isArray(req.params) || - !(req.params.length >= 2) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 2) ) { throw rpcErrors.invalidInput(); } - const params = req.params as [ + const params = request.params as [ Record[], string, Record?, ]; const message = params[0]; - const address = await validateAndNormalizeKeyholder(params[1], req); + const address = await validateAndNormalizeKeyholder(params[1], request); const version = 'V1'; const extraParams = params[2] || {}; const msgParams: TypedMessageV1Params = { @@ -254,27 +227,26 @@ WalletMiddlewareOptions): JsonRpcMiddleware { version, }; - res.result = await processTypedMessage(msgParams, req, version); + return await processTypedMessage(msgParams, request, version); } - async function signTypedDataV3( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + async function signTypedDataV3({ + request, + }: MiddlewareParams): Promise { if (!processTypedMessageV3) { throw rpcErrors.methodNotSupported(); } if ( - !req?.params || - !Array.isArray(req.params) || - !(req.params.length >= 2) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 2) ) { throw rpcErrors.invalidInput(); } - const params = req.params as [string, string]; + const params = request.params as [string, string]; - const address = await validateAndNormalizeKeyholder(params[0], req); + const address = await validateAndNormalizeKeyholder(params[0], request); const message = normalizeTypedMessage(params[1]); validatePrimaryType(message); validateVerifyingContract(message); @@ -286,27 +258,26 @@ WalletMiddlewareOptions): JsonRpcMiddleware { signatureMethod: 'eth_signTypedData_v3', }; - res.result = await processTypedMessageV3(msgParams, req, version); + return await processTypedMessageV3(msgParams, request, version); } - async function signTypedDataV4( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + async function signTypedDataV4({ + request, + }: MiddlewareParams): Promise { if (!processTypedMessageV4) { throw rpcErrors.methodNotSupported(); } if ( - !req?.params || - !Array.isArray(req.params) || - !(req.params.length >= 2) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 2) ) { throw rpcErrors.invalidInput(); } - const params = req.params as [string, string]; + const params = request.params as [string, string]; - const address = await validateAndNormalizeKeyholder(params[0], req); + const address = await validateAndNormalizeKeyholder(params[0], request); const message = normalizeTypedMessage(params[1]); validatePrimaryType(message); validateVerifyingContract(message); @@ -318,25 +289,24 @@ WalletMiddlewareOptions): JsonRpcMiddleware { signatureMethod: 'eth_signTypedData_v4', }; - res.result = await processTypedMessageV4(msgParams, req, version); + return await processTypedMessageV4(msgParams, request, version); } - async function personalSign( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + async function personalSign({ + request, + }: MiddlewareParams): Promise { if (!processPersonalMessage) { throw rpcErrors.methodNotSupported(); } if ( - !req?.params || - !Array.isArray(req.params) || - !(req.params.length >= 2) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 2) ) { throw rpcErrors.invalidInput(); } - const params = req.params as [string, string, TransactionParams?]; + const params = request.params as [string, string, TransactionParams?]; // process normally const firstParam = params[0]; @@ -353,19 +323,13 @@ WalletMiddlewareOptions): JsonRpcMiddleware { // and the second param is definitely not, but is hex. let address: string, message: string; if (resemblesAddress(firstParam) && !resemblesAddress(secondParam)) { - let warning = `The eth_personalSign method requires params ordered `; - warning += `[message, address]. This was previously handled incorrectly, `; - warning += `and has been corrected automatically. `; - warning += `Please switch this param order for smooth behavior in the future.`; - (res as any).warning = warning; - address = firstParam; message = secondParam; } else { message = firstParam; address = secondParam; } - address = await validateAndNormalizeKeyholder(address, req); + address = await validateAndNormalizeKeyholder(address, request); const msgParams: MessageParams = { ...extraParams, @@ -374,22 +338,21 @@ WalletMiddlewareOptions): JsonRpcMiddleware { signatureMethod: 'personal_sign', }; - res.result = await processPersonalMessage(msgParams, req); + return await processPersonalMessage(msgParams, request); } - async function personalRecover( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + async function personalRecover({ + request, + }: MiddlewareParams): Promise { if ( - !req?.params || - !Array.isArray(req.params) || - !(req.params.length >= 2) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 2) ) { throw rpcErrors.invalidInput(); } - const params = req.params as [string, string]; + const params = request.params as [string, string]; const message = params[0]; const signature = params[1]; const signerAddress = sigUtil.recoverPersonalSignature({ @@ -397,49 +360,50 @@ WalletMiddlewareOptions): JsonRpcMiddleware { signature, }); - res.result = signerAddress; + return signerAddress; } - async function encryptionPublicKey( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + async function encryptionPublicKey({ + request, + }: MiddlewareParams): Promise { if (!processEncryptionPublicKey) { throw rpcErrors.methodNotSupported(); } if ( - !req?.params || - !Array.isArray(req.params) || - !(req.params.length >= 1) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 1) ) { throw rpcErrors.invalidInput(); } - const params = req.params as [string]; + const params = request.params as [string]; - const address = await validateAndNormalizeKeyholder(params[0], req); + const address = await validateAndNormalizeKeyholder(params[0], request); - res.result = await processEncryptionPublicKey(address, req); + return await processEncryptionPublicKey(address, request); } - async function decryptMessage( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + async function decryptMessage({ + request, + }: MiddlewareParams): Promise { if (!processDecryptMessage) { throw rpcErrors.methodNotSupported(); } if ( - !req?.params || - !Array.isArray(req.params) || - !(req.params.length >= 1) + !request.params || + !Array.isArray(request.params) || + !(request.params.length >= 1) ) { throw rpcErrors.invalidInput(); } - const params = req.params as [string, string, Record?]; + const params = request.params as [string, string, Record?]; const ciphertext: string = params[0]; - const address: string = await validateAndNormalizeKeyholder(params[1], req); + const address: string = await validateAndNormalizeKeyholder( + params[1], + request, + ); const extraParams = params[2] || {}; const msgParams: MessageParams = { ...extraParams, @@ -447,7 +411,7 @@ WalletMiddlewareOptions): JsonRpcMiddleware { data: ciphertext, }; - res.result = await processDecryptMessage(msgParams, req); + return await processDecryptMessage(msgParams, request); } // From 929218060c583c874fac848bc64d1fafb767dc43 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:39:54 -0700 Subject: [PATCH 10/30] refactor: Migrate create-network-client --- .../src/providerAsMiddleware.test.ts | 6 +- .../src/NetworkController.ts | 2 + .../src/create-network-client.ts | 126 ++++++++++-------- .../tests/NetworkController.test.ts | 1 + 4 files changed, 77 insertions(+), 58 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts index d404b5c4b19..ce1376a304e 100644 --- a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts @@ -1,4 +1,4 @@ -import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import type { Json } from '@metamask/utils'; @@ -10,10 +10,10 @@ import { } from './providerAsMiddleware'; import { createRequest } from '../test/util/helpers'; -const createMockProvider = (result: Json): SafeEventEmitterProvider => +const createMockProvider = (result: Json): InternalProvider => ({ request: jest.fn().mockResolvedValue(result), - }) as unknown as SafeEventEmitterProvider; + }) as unknown as InternalProvider; describe('providerAsMiddleware', () => { it('forwards requests to the provider and returns the result', async () => { diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index e8204301abc..e8e5a88295a 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -2446,6 +2446,8 @@ export class NetworkController extends BaseController< * * In-progress requests will not be aborted. */ + // We're intentionally changing the signature of an extended method. + // eslint-disable-next-line @typescript-eslint/no-misused-promises async destroy() { await this.#blockTrackerProxy?.destroy(); } diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index bc95fdd38eb..160aa99c6d3 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -12,18 +12,18 @@ import { createFetchMiddleware, createRetryOnEmptyMiddleware, } from '@metamask/eth-json-rpc-middleware'; +import { InternalProvider } from '@metamask/eth-json-rpc-provider'; +import { providerFromMiddlewareV2 } from '@metamask/eth-json-rpc-provider'; +import { asV2Middleware } from '@metamask/json-rpc-engine'; import { - InternalProvider, - providerFromMiddleware, -} from '@metamask/eth-json-rpc-provider'; -import { - createAsyncMiddleware, createScaffoldMiddleware, - JsonRpcEngine, - mergeMiddleware, -} from '@metamask/json-rpc-engine'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import type { Hex, Json, JsonRpcParams } from '@metamask/utils'; + JsonRpcEngineV2, +} from '@metamask/json-rpc-engine/v2'; +import type { + JsonRpcMiddleware, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; +import type { Hex, Json, JsonRpcRequest } from '@metamask/utils'; import type { Logger } from 'loglevel'; import type { NetworkControllerMessenger } from './NetworkController'; @@ -49,6 +49,12 @@ export type NetworkClient = { destroy: () => void; }; +type RpcApiMiddleware = JsonRpcMiddleware< + JsonRpcRequest, + Json, + MiddlewareContext<{ origin: string }> +>; + /** * Create a JSON RPC network client for a specific network. * @@ -136,17 +142,21 @@ export function createNetworkClient({ }); }); - const rpcApiMiddleware = - configuration.type === NetworkClientType.Infura - ? createInfuraMiddleware({ - rpcService: rpcServiceChain, - options: { - source: 'metamask', - }, - }) - : createFetchMiddleware({ rpcService: rpcServiceChain }); + let rpcApiMiddleware: RpcApiMiddleware; + if (configuration.type === NetworkClientType.Infura) { + rpcApiMiddleware = asV2Middleware( + createInfuraMiddleware({ + rpcService: rpcServiceChain, + options: { + source: 'metamask', + }, + }), + ) as unknown as RpcApiMiddleware; + } else { + rpcApiMiddleware = createFetchMiddleware({ rpcService: rpcServiceChain }); + } - const rpcProvider = providerFromMiddleware(rpcApiMiddleware); + const rpcProvider = providerFromMiddlewareV2(rpcApiMiddleware); const blockTracker = createBlockTracker({ networkClientType: configuration.type, @@ -169,11 +179,11 @@ export function createNetworkClient({ rpcApiMiddleware, }); - const engine = new JsonRpcEngine(); - - engine.push(networkMiddleware); - - const provider: Provider = new InternalProvider({ engine }); + const provider: Provider = new InternalProvider({ + engine: JsonRpcEngineV2.create({ + middleware: [networkMiddleware], + }), + }); const destroy = () => { // TODO: Either fix this lint violation or explain why it's necessary to ignore. @@ -240,17 +250,19 @@ function createInfuraNetworkMiddleware({ blockTracker: PollingBlockTracker; network: InfuraNetworkType; rpcProvider: InternalProvider; - rpcApiMiddleware: JsonRpcMiddleware; + rpcApiMiddleware: RpcApiMiddleware; }) { - return mergeMiddleware([ - createNetworkAndChainIdMiddleware({ network }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), - createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), - createBlockTrackerInspectorMiddleware({ blockTracker }), - rpcApiMiddleware, - ]); + return JsonRpcEngineV2.create({ + middleware: [ + createNetworkAndChainIdMiddleware({ network }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), + createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ], + }).asMiddleware(); } /** @@ -272,11 +284,10 @@ function createNetworkAndChainIdMiddleware({ const createChainIdMiddleware = ( chainId: Hex, -): JsonRpcMiddleware => { - return (req, res, next, end) => { - if (req.method === 'eth_chainId') { - res.result = chainId; - return end(); +): JsonRpcMiddleware => { + return ({ request, next }) => { + if (request.method === 'eth_chainId') { + return chainId; } return next(); }; @@ -298,21 +309,23 @@ function createCustomNetworkMiddleware({ }: { blockTracker: PollingBlockTracker; chainId: Hex; - rpcApiMiddleware: JsonRpcMiddleware; -}): JsonRpcMiddleware { + rpcApiMiddleware: RpcApiMiddleware; +}) { const testMiddlewares = process.env.IN_TEST ? [createEstimateGasDelayTestMiddleware()] : []; - return mergeMiddleware([ - ...testMiddlewares, - createChainIdMiddleware(chainId), - createBlockRefRewriteMiddleware({ blockTracker }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockTrackerInspectorMiddleware({ blockTracker }), - rpcApiMiddleware, - ]); + return JsonRpcEngineV2.create({ + middleware: [ + ...testMiddlewares, + createChainIdMiddleware(chainId), + createBlockRefRewriteMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ], + }).asMiddleware(); } /** @@ -321,11 +334,14 @@ function createCustomNetworkMiddleware({ * * @returns The middleware for delaying gas estimation calls by 2 seconds when in test. */ -function createEstimateGasDelayTestMiddleware() { - return createAsyncMiddleware(async (req, _, next) => { - if (req.method === 'eth_estimateGas') { +function createEstimateGasDelayTestMiddleware(): JsonRpcMiddleware< + JsonRpcRequest, + Json +> { + return async ({ request, next }) => { + if (request.method === 'eth_estimateGas') { await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); } return next(); - }); + }; } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 18cbac232a2..1b7b36da05b 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -16804,6 +16804,7 @@ async function waitForPublishedEvents({ if (interestingEventPayloads.length === expectedNumberOfEvents) { resolve(interestingEventPayloads); } else { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject( new Error( `Expected to receive ${expectedNumberOfEvents} ${String(eventType)} event(s), but received ${ From a489225f1fa0ff5754693733bf00e663c47345ec Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:46:07 -0800 Subject: [PATCH 11/30] refactor: Fix getAccounts --- ...llet-request-execution-permissions.test.ts | 6 +- .../wallet-request-execution-permissions.ts | 3 +- ...wallet-revoke-execution-permission.test.ts | 6 +- .../wallet-revoke-execution-permission.ts | 3 +- .../src/utils/validation.test.ts | 19 ++- .../src/utils/validation.ts | 9 +- .../src/wallet.test.ts | 120 ++++++++++-------- .../eth-json-rpc-middleware/src/wallet.ts | 77 ++++++----- .../test/util/helpers.ts | 16 +++ .../json-rpc-engine/src/v2/JsonRpcEngineV2.ts | 2 +- .../src/v2/createScaffoldMiddleware.ts | 25 ++-- 11 files changed, 176 insertions(+), 110 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts index bcbb8d19009..71600b8c598 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts @@ -1,4 +1,3 @@ -import type { MiddlewareParams } from '@metamask/json-rpc-engine/v2'; import type { JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona'; @@ -8,6 +7,7 @@ import type { RequestExecutionPermissionsResult, } from './wallet-request-execution-permissions'; import { createWalletRequestExecutionPermissionsHandler } from './wallet-request-execution-permissions'; +import type { WalletMiddlewareParams } from '../wallet'; const ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123a'; const CHAIN_ID_MOCK = '0x1'; @@ -74,7 +74,7 @@ describe('wallet_requestExecutionPermissions', () => { processRequestExecutionPermissions: processRequestExecutionPermissionsMock, }); - return handler({ request } as MiddlewareParams); + return handler({ request } as WalletMiddlewareParams); }; beforeEach(() => { @@ -126,7 +126,7 @@ describe('wallet_requestExecutionPermissions', () => { await expect( createWalletRequestExecutionPermissionsHandler({})({ request, - } as MiddlewareParams), + } as WalletMiddlewareParams), ).rejects.toThrow( `wallet_requestExecutionPermissions - no middleware configured`, ); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts index 7291a56e184..818da8be660 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts @@ -21,6 +21,7 @@ import { } from '@metamask/utils'; import { validateParams } from '../utils/validation'; +import type { WalletMiddlewareContext } from '../wallet'; const PermissionStruct = object({ type: string(), @@ -70,7 +71,7 @@ export function createWalletRequestExecutionPermissionsHandler({ processRequestExecutionPermissions, }: { processRequestExecutionPermissions?: ProcessRequestExecutionPermissionsHook; -}): JsonRpcMiddleware { +}): JsonRpcMiddleware { return async ({ request }) => { if (!processRequestExecutionPermissions) { throw rpcErrors.methodNotSupported( diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts index ba087f27599..a2d2fa2999c 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts @@ -1,4 +1,3 @@ -import type { MiddlewareParams } from '@metamask/json-rpc-engine/v2'; import type { JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona'; @@ -7,6 +6,7 @@ import type { RevokeExecutionPermissionRequestParams, } from './wallet-revoke-execution-permission'; import { createWalletRevokeExecutionPermissionHandler } from './wallet-revoke-execution-permission'; +import type { WalletMiddlewareParams } from '../wallet'; const HEX_MOCK = '0x123abc'; @@ -25,7 +25,7 @@ describe('wallet_revokeExecutionPermission', () => { const handler = createWalletRevokeExecutionPermissionHandler({ processRevokeExecutionPermission: processRevokeExecutionPermissionMock, }); - return handler({ request } as MiddlewareParams); + return handler({ request } as WalletMiddlewareParams); }; beforeEach(() => { @@ -55,7 +55,7 @@ describe('wallet_revokeExecutionPermission', () => { await expect( createWalletRevokeExecutionPermissionHandler({})({ request, - } as MiddlewareParams), + } as WalletMiddlewareParams), ).rejects.toThrow( 'wallet_revokeExecutionPermission - no middleware configured', ); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts index a9e6069e742..274d5e031a2 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts @@ -6,6 +6,7 @@ import type { Json } from '@metamask/utils'; import { type JsonRpcRequest, StrictHexStruct } from '@metamask/utils'; import { validateParams } from '../utils/validation'; +import type { WalletMiddlewareContext } from '../wallet'; export const RevokeExecutionPermissionResultStruct = object({}); @@ -30,7 +31,7 @@ export function createWalletRevokeExecutionPermissionHandler({ processRevokeExecutionPermission, }: { processRevokeExecutionPermission?: ProcessRevokeExecutionPermissionHook; -}): JsonRpcMiddleware { +}): JsonRpcMiddleware { return async ({ request }) => { if (!processRevokeExecutionPermission) { throw rpcErrors.methodNotSupported( diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts index d781790da1e..903553453c4 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts @@ -1,7 +1,7 @@ +import { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; import { providerErrors } from '@metamask/rpc-errors'; import type { StructError } from '@metamask/superstruct'; import { any, validate } from '@metamask/superstruct'; -import type { JsonRpcRequest } from '@metamask/utils'; import { resemblesAddress, @@ -15,7 +15,8 @@ jest.mock('@metamask/superstruct', () => ({ })); const ADDRESS_MOCK = '0xABCDabcdABCDabcdABCDabcdABCDabcdABCDabcd'; -const REQUEST_MOCK = {} as JsonRpcRequest; +const createContext = () => + new MiddlewareContext<{ origin: string }>([['origin', 'test']]); const STRUCT_ERROR_MOCK = { failures: () => [ @@ -33,9 +34,7 @@ const STRUCT_ERROR_MOCK = { describe('Validation Utils', () => { const validateMock = jest.mocked(validate); - let getAccountsMock: jest.MockedFn< - (req: JsonRpcRequest) => Promise - >; + let getAccountsMock: jest.MockedFn<(origin: string) => Promise>; beforeEach(() => { jest.resetAllMocks(); @@ -47,7 +46,7 @@ describe('Validation Utils', () => { it('returns lowercase address', async () => { const result = await validateAndNormalizeKeyholder( ADDRESS_MOCK, - REQUEST_MOCK, + createContext(), { getAccounts: getAccountsMock, }, @@ -60,7 +59,7 @@ describe('Validation Utils', () => { getAccountsMock.mockResolvedValueOnce([]); await expect( - validateAndNormalizeKeyholder(ADDRESS_MOCK, REQUEST_MOCK, { + validateAndNormalizeKeyholder(ADDRESS_MOCK, createContext(), { getAccounts: getAccountsMock, }), ).rejects.toThrow(providerErrors.unauthorized()); @@ -68,7 +67,7 @@ describe('Validation Utils', () => { it('throws if address is not string', async () => { await expect( - validateAndNormalizeKeyholder(123 as never, REQUEST_MOCK, { + validateAndNormalizeKeyholder(123 as never, createContext(), { getAccounts: getAccountsMock, }), ).rejects.toThrow( @@ -78,7 +77,7 @@ describe('Validation Utils', () => { it('throws if address is empty string', async () => { await expect( - validateAndNormalizeKeyholder('' as never, REQUEST_MOCK, { + validateAndNormalizeKeyholder('' as never, createContext(), { getAccounts: getAccountsMock, }), ).rejects.toThrow( @@ -88,7 +87,7 @@ describe('Validation Utils', () => { it('throws if address length is not 40', async () => { await expect( - validateAndNormalizeKeyholder('0x123', REQUEST_MOCK, { + validateAndNormalizeKeyholder('0x123', createContext(), { getAccounts: getAccountsMock, }), ).rejects.toThrow( diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.ts b/packages/eth-json-rpc-middleware/src/utils/validation.ts index 0913574c4d7..d4f85096198 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.ts @@ -1,12 +1,13 @@ +import type { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { Struct, StructError } from '@metamask/superstruct'; import { validate } from '@metamask/superstruct'; -import type { Hex, JsonRpcRequest } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; export async function validateAndNormalizeKeyholder( address: Hex, - req: JsonRpcRequest, - { getAccounts }: { getAccounts: (req: JsonRpcRequest) => Promise }, + context: MiddlewareContext<{ origin: string }>, + { getAccounts }: { getAccounts: (origin: string) => Promise }, ): Promise { if ( typeof address === 'string' && @@ -15,7 +16,7 @@ export async function validateAndNormalizeKeyholder( ) { // Ensure that an "unauthorized" error is thrown if the requester // does not have the `eth_accounts` permission. - const accounts = await getAccounts(req); + const accounts = await getAccounts(context.assertGet('origin')); const normalizedAccounts: string[] = accounts.map((_address) => _address.toLowerCase(), diff --git a/packages/eth-json-rpc-middleware/src/wallet.test.ts b/packages/eth-json-rpc-middleware/src/wallet.test.ts index 7e235027727..cac282209ff 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.test.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.test.ts @@ -7,7 +7,7 @@ import type { TypedMessageV1Params, } from '.'; import { createWalletMiddleware } from '.'; -import { createRequest } from '../test/util/helpers'; +import { createHandleParams, createRequest } from '../test/util/helpers'; const testAddresses = [ '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', @@ -27,7 +27,7 @@ describe('wallet', () => { middleware: [createWalletMiddleware({ getAccounts })], }); const coinbaseResult = await engine.handle( - createRequest({ + ...createHandleParams({ method: 'eth_coinbase', }), ); @@ -40,7 +40,7 @@ describe('wallet', () => { middleware: [createWalletMiddleware({ getAccounts })], }); const coinbaseResult = await engine.handle( - createRequest({ + ...createHandleParams({ method: 'eth_coinbase', }), ); @@ -53,7 +53,7 @@ describe('wallet', () => { middleware: [createWalletMiddleware({ getAccounts })], }); const coinbaseResult = await engine.handle( - createRequest({ + ...createHandleParams({ method: 'eth_coinbase', }), ); @@ -77,9 +77,9 @@ describe('wallet', () => { const txParams = { from: testAddresses[0], }; - const payload = { method: 'eth_sendTransaction', params: [txParams] }; - const sendTxResult = await engine.handle(createRequest(payload)); + + const sendTxResult = await engine.handle(...createHandleParams(payload)); expect(sendTxResult).toBeDefined(); expect(sendTxResult).toStrictEqual(testTxHash); expect(witnessedTxParams).toHaveLength(1); @@ -126,13 +126,14 @@ describe('wallet', () => { const txParams = { from: testUnkownAddress, }; - - const payload = createRequest({ + const payload = { method: 'eth_sendTransaction', params: [txParams], - }); - const promise = engine.handle(payload); - await expect(promise).rejects.toThrow( + }; + + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); @@ -153,12 +154,12 @@ describe('wallet', () => { from: testAddresses[0], to: testAddresses[1], }; - - const payload = createRequest({ + const payload = { method: 'eth_sendTransaction', params: [txParams], - }); - await engine.handle(payload); + }; + + await engine.handle(...createHandleParams(payload)); expect(witnessedTxParams).toHaveLength(1); expect(witnessedTxParams[0]).toStrictEqual(txParams); }); @@ -180,11 +181,11 @@ describe('wallet', () => { const txParams = { from: testAddresses[0], }; - const payload = { method: 'eth_signTransaction', params: [txParams] }; - const sendTxResult = await engine.handle(createRequest(payload)); - expect(sendTxResult).toBeDefined(); - expect(sendTxResult).toStrictEqual(testTxHash); + + expect(await engine.handle(...createHandleParams(payload))).toStrictEqual( + testTxHash, + ); expect(witnessedTxParams).toHaveLength(1); expect(witnessedTxParams[0]).toStrictEqual(txParams); }); @@ -205,9 +206,9 @@ describe('wallet', () => { from: testAddresses[0], to: testAddresses[1], }; - const payload = { method: 'eth_signTransaction', params: [txParams] }; - await engine.handle(createRequest(payload)); + + await engine.handle(...createHandleParams(payload)); expect(witnessedTxParams).toHaveLength(1); expect(witnessedTxParams[0]).toStrictEqual(txParams); }); @@ -227,9 +228,11 @@ describe('wallet', () => { const txParams = { from: '0x3', }; - const payload = { method: 'eth_signTransaction', params: [txParams] }; - await expect(engine.handle(createRequest(payload))).rejects.toThrow( + + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( new Error('Invalid parameters: must provide an Ethereum address.'), ); }); @@ -249,10 +252,11 @@ describe('wallet', () => { const txParams = { from: testUnkownAddress, }; - const payload = { method: 'eth_signTransaction', params: [txParams] }; - const promise = engine.handle(createRequest(payload)); - await expect(promise).rejects.toThrow( + + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); @@ -283,7 +287,7 @@ describe('wallet', () => { method: 'eth_signTypedData', params: [message, testAddresses[0]], }; - const signMsgResult = await engine.handle(createRequest(payload)); + const signMsgResult = await engine.handle(...createHandleParams(payload)); expect(signMsgResult).toBeDefined(); expect(signMsgResult).toStrictEqual(testMsgSig); @@ -320,7 +324,9 @@ describe('wallet', () => { method: 'eth_signTypedData', params: [message, '0x3d'], }; - await expect(engine.handle(createRequest(payload))).rejects.toThrow( + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( new Error('Invalid parameters: must provide an Ethereum address.'), ); }); @@ -344,13 +350,14 @@ describe('wallet', () => { value: 'Hi, Alice!', }, ]; - const payload = { method: 'eth_signTypedData', params: [message, testUnkownAddress], }; - const promise = engine.handle(createRequest(payload)); - await expect(promise).rejects.toThrow( + + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); @@ -400,7 +407,9 @@ describe('wallet', () => { params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup }; - const signTypedDataV3Result = await engine.handle(createRequest(payload)); + const signTypedDataV3Result = await engine.handle( + ...createHandleParams(payload), + ); expect(signTypedDataV3Result).toBeDefined(); expect(signTypedDataV3Result).toStrictEqual(testMsgSig); @@ -450,8 +459,9 @@ describe('wallet', () => { params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup }; - const promise = engine.handle(createRequest(payload)); - await expect(promise).rejects.toThrow('Invalid input.'); + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow('Invalid input.'); }); it('should not throw if verifyingContract is undefined', async () => { @@ -488,7 +498,7 @@ describe('wallet', () => { params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup }; - const result = await engine.handle(createRequest(payload)); + const result = await engine.handle(...createHandleParams(payload)); expect(result).toBe( '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', ); @@ -548,7 +558,7 @@ describe('wallet', () => { params: [testAddresses[0], JSON.stringify(getMsgParams())], }; - const result = await engine.handle(createRequest(payload)); + const result = await engine.handle(...createHandleParams(payload)); expect(result).toBe( '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', ); @@ -578,8 +588,9 @@ describe('wallet', () => { ], }; - const promise = engine.handle(createRequest(payload)); - await expect(promise).rejects.toThrow('Invalid input.'); + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow('Invalid input.'); }); it('should not throw if request is permit with undefined value for verifyingContract address', async () => { @@ -601,7 +612,7 @@ describe('wallet', () => { params: [testAddresses[0], JSON.stringify(getMsgParams())], }; - const result = await engine.handle(createRequest(payload)); + const result = await engine.handle(...createHandleParams(payload)); expect(result).toBe( '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', ); @@ -626,7 +637,7 @@ describe('wallet', () => { params: [testAddresses[0], JSON.stringify(getMsgParams('cosmos'))], }; - const result = await engine.handle(createRequest(payload)); + const result = await engine.handle(...createHandleParams(payload)); expect(result).toBe( '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', ); @@ -655,8 +666,9 @@ describe('wallet', () => { ], }; - const promise = engine.handle(createRequest(payload)); - await expect(promise).rejects.toThrow('Invalid input.'); + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow('Invalid input.'); }); it('should throw if type of primaryType is not defined', async () => { @@ -685,8 +697,9 @@ describe('wallet', () => { ], }; - const promise = engine.handle(createRequest(payload)); - await expect(promise).rejects.toThrow('Invalid input.'); + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow('Invalid input.'); }); }); @@ -709,7 +722,7 @@ describe('wallet', () => { method: 'personal_sign', params: [message, testAddresses[0]], }; - const signMsgResult = await engine.handle(createRequest(payload)); + const signMsgResult = await engine.handle(...createHandleParams(payload)); expect(signMsgResult).toBeDefined(); expect(signMsgResult).toStrictEqual(testMsgSig); @@ -740,7 +753,9 @@ describe('wallet', () => { params: [message, '0x3d'], }; - await expect(engine.handle(createRequest(payload))).rejects.toThrow( + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( new Error('Invalid parameters: must provide an Ethereum address.'), ); }); @@ -764,8 +779,9 @@ describe('wallet', () => { params: [message, testUnkownAddress], }; - const promise = engine.handle(createRequest(payload)); - await expect(promise).rejects.toThrow( + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); @@ -791,7 +807,9 @@ describe('wallet', () => { method: 'personal_ecRecover', params: [signParams.message, signParams.signature], }; - const ecrecoverResult = await engine.handle(createRequest(payload)); + const ecrecoverResult = await engine.handle( + ...createHandleParams(payload), + ); expect(ecrecoverResult).toBeDefined(); expect(ecrecoverResult).toStrictEqual(signParams.addressHex); }); @@ -817,7 +835,9 @@ describe('wallet', () => { method: 'personal_ecRecover', params: [signParams.message, signParams.signature], }; - const ecrecoverResult = await engine.handle(createRequest(payload)); + const ecrecoverResult = await engine.handle( + ...createHandleParams(payload), + ); expect(ecrecoverResult).toBeDefined(); expect(ecrecoverResult).toStrictEqual(signParams.addressHex); }); diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index b6e129f648d..0c12fb7658a 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -1,6 +1,7 @@ import * as sigUtil from '@metamask/eth-sig-util'; import type { JsonRpcMiddleware, + MiddlewareContext, MiddlewareParams, } from '@metamask/json-rpc-engine/v2'; import { createScaffoldMiddleware } from '@metamask/json-rpc-engine/v2'; @@ -41,7 +42,7 @@ export type TypedMessageV1Params = Omit & { }; export type WalletMiddlewareOptions = { - getAccounts: (req: JsonRpcRequest) => Promise; + getAccounts: (origin: string) => Promise; processDecryptMessage?: ( msgParams: MessageParams, req: JsonRpcRequest, @@ -81,6 +82,12 @@ export type WalletMiddlewareOptions = { processRevokeExecutionPermission?: ProcessRevokeExecutionPermissionHook; }; +export type WalletMiddlewareContext = MiddlewareContext<{ origin: string }>; +export type WalletMiddlewareParams = MiddlewareParams< + JsonRpcRequest, + WalletMiddlewareContext +>; + export function createWalletMiddleware({ getAccounts, processDecryptMessage, @@ -93,12 +100,16 @@ export function createWalletMiddleware({ processTypedMessageV4, processRequestExecutionPermissions, processRevokeExecutionPermission, -}: WalletMiddlewareOptions): JsonRpcMiddleware { +}: WalletMiddlewareOptions): JsonRpcMiddleware< + JsonRpcRequest, + Json, + WalletMiddlewareContext +> { if (!getAccounts) { throw new Error('opts.getAccounts is required'); } - return createScaffoldMiddleware({ + return createScaffoldMiddleware({ // account lookups eth_accounts: lookupAccounts, eth_coinbase: lookupDefaultAccount, @@ -132,15 +143,15 @@ export function createWalletMiddleware({ // async function lookupAccounts({ - request, - }: MiddlewareParams): Promise { - return await getAccounts(request); + context, + }: WalletMiddlewareParams): Promise { + return await getAccounts(context.assertGet('origin')); } async function lookupDefaultAccount({ - request, - }: MiddlewareParams): Promise { - const accounts = await getAccounts(request); + context, + }: WalletMiddlewareParams): Promise { + const accounts = await getAccounts(context.assertGet('origin')); return accounts[0] || null; } @@ -150,7 +161,8 @@ export function createWalletMiddleware({ async function sendTransaction({ request, - }: MiddlewareParams): Promise { + context, + }: WalletMiddlewareParams): Promise { if (!processTransaction) { throw rpcErrors.methodNotSupported(); } @@ -165,14 +177,15 @@ export function createWalletMiddleware({ const params = request.params[0] as TransactionParams | undefined; const txParams: TransactionParams = { ...params, - from: await validateAndNormalizeKeyholder(params?.from || '', request), + from: await validateAndNormalizeKeyholder(params?.from || '', context), }; return await processTransaction(txParams, request); } async function signTransaction({ request, - }: MiddlewareParams): Promise { + context, + }: WalletMiddlewareParams): Promise { if (!processSignTransaction) { throw rpcErrors.methodNotSupported(); } @@ -187,7 +200,7 @@ export function createWalletMiddleware({ const params = request.params[0] as TransactionParams | undefined; const txParams: TransactionParams = { ...params, - from: await validateAndNormalizeKeyholder(params?.from || '', request), + from: await validateAndNormalizeKeyholder(params?.from || '', context), }; return await processSignTransaction(txParams, request); } @@ -198,7 +211,8 @@ export function createWalletMiddleware({ async function signTypedData({ request, - }: MiddlewareParams): Promise { + context, + }: WalletMiddlewareParams): Promise { if (!processTypedMessage) { throw rpcErrors.methodNotSupported(); } @@ -216,7 +230,7 @@ export function createWalletMiddleware({ Record?, ]; const message = params[0]; - const address = await validateAndNormalizeKeyholder(params[1], request); + const address = await validateAndNormalizeKeyholder(params[1], context); const version = 'V1'; const extraParams = params[2] || {}; const msgParams: TypedMessageV1Params = { @@ -232,7 +246,8 @@ export function createWalletMiddleware({ async function signTypedDataV3({ request, - }: MiddlewareParams): Promise { + context, + }: WalletMiddlewareParams): Promise { if (!processTypedMessageV3) { throw rpcErrors.methodNotSupported(); } @@ -246,7 +261,7 @@ export function createWalletMiddleware({ const params = request.params as [string, string]; - const address = await validateAndNormalizeKeyholder(params[0], request); + const address = await validateAndNormalizeKeyholder(params[0], context); const message = normalizeTypedMessage(params[1]); validatePrimaryType(message); validateVerifyingContract(message); @@ -263,7 +278,8 @@ export function createWalletMiddleware({ async function signTypedDataV4({ request, - }: MiddlewareParams): Promise { + context, + }: WalletMiddlewareParams): Promise { if (!processTypedMessageV4) { throw rpcErrors.methodNotSupported(); } @@ -277,7 +293,7 @@ export function createWalletMiddleware({ const params = request.params as [string, string]; - const address = await validateAndNormalizeKeyholder(params[0], request); + const address = await validateAndNormalizeKeyholder(params[0], context); const message = normalizeTypedMessage(params[1]); validatePrimaryType(message); validateVerifyingContract(message); @@ -294,7 +310,8 @@ export function createWalletMiddleware({ async function personalSign({ request, - }: MiddlewareParams): Promise { + context, + }: WalletMiddlewareParams): Promise { if (!processPersonalMessage) { throw rpcErrors.methodNotSupported(); } @@ -329,7 +346,7 @@ export function createWalletMiddleware({ message = firstParam; address = secondParam; } - address = await validateAndNormalizeKeyholder(address, request); + address = await validateAndNormalizeKeyholder(address, context); const msgParams: MessageParams = { ...extraParams, @@ -343,7 +360,7 @@ export function createWalletMiddleware({ async function personalRecover({ request, - }: MiddlewareParams): Promise { + }: WalletMiddlewareParams): Promise { if ( !request.params || !Array.isArray(request.params) || @@ -365,7 +382,8 @@ export function createWalletMiddleware({ async function encryptionPublicKey({ request, - }: MiddlewareParams): Promise { + context, + }: WalletMiddlewareParams): Promise { if (!processEncryptionPublicKey) { throw rpcErrors.methodNotSupported(); } @@ -379,14 +397,15 @@ export function createWalletMiddleware({ const params = request.params as [string]; - const address = await validateAndNormalizeKeyholder(params[0], request); + const address = await validateAndNormalizeKeyholder(params[0], context); return await processEncryptionPublicKey(address, request); } async function decryptMessage({ request, - }: MiddlewareParams): Promise { + context, + }: WalletMiddlewareParams): Promise { if (!processDecryptMessage) { throw rpcErrors.methodNotSupported(); } @@ -402,7 +421,7 @@ export function createWalletMiddleware({ const ciphertext: string = params[0]; const address: string = await validateAndNormalizeKeyholder( params[1], - request, + context, ); const extraParams = params[2] || {}; const msgParams: MessageParams = { @@ -423,15 +442,15 @@ export function createWalletMiddleware({ * copy of it. * * @param address - The address to validate and normalize. - * @param req - The request object. + * @param context - The context of the request. * @returns The normalized address, if valid. Otherwise, throws * an error */ async function validateAndNormalizeKeyholder( address: string, - req: JsonRpcRequest, + context: WalletMiddlewareContext, ): Promise { - return validateKeyholder(address as Hex, req, { getAccounts }); + return validateKeyholder(address as Hex, context, { getAccounts }); } } diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index a1a691b779d..cbadde4daee 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -22,6 +22,22 @@ export const createRequest = < } as Output; }; +const createHandleOptions = () => ({ + context: { + origin: 'test', + }, +}); + +export const createHandleParams = < + Input extends Partial>, + Output extends Input & JsonRpcRequest, +>( + request: Input, +): [Output, ReturnType] => [ + createRequest(request), + createHandleOptions(), +]; + /** * An object that can be used to assign a canned result to a request made via * `provider.request`. diff --git a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts index c9431b84959..43eabd5c51b 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts @@ -44,7 +44,7 @@ export type Next = ( export type MiddlewareParams< Request extends JsonRpcCall = JsonRpcCall, - Context extends MiddlewareContext = MiddlewareContext, + Context extends ContextConstraint = MiddlewareContext, > = { request: Readonly; context: Context; diff --git a/packages/json-rpc-engine/src/v2/createScaffoldMiddleware.ts b/packages/json-rpc-engine/src/v2/createScaffoldMiddleware.ts index 88c7212db9f..6eaf1bdc450 100644 --- a/packages/json-rpc-engine/src/v2/createScaffoldMiddleware.ts +++ b/packages/json-rpc-engine/src/v2/createScaffoldMiddleware.ts @@ -1,23 +1,32 @@ import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import type { JsonRpcMiddleware } from './JsonRpcEngineV2'; +import type { ContextConstraint, MiddlewareContext } from './MiddlewareContext'; // Only permit primitive values as hard-coded scaffold middleware results. type JsonPrimitive = string | number | boolean | null; +/** + * A handler for a scaffold middleware function. + * + * @template Params - The parameters of the request. + * @template Result - The result of the request. + * @template Context - The context of the request. + * @returns A JSON-RPC middleware function or a primitive JSON value. + */ export type ScaffoldMiddlewareHandler< Params extends JsonRpcParams, Result extends Json, -> = JsonRpcMiddleware, Result> | JsonPrimitive; + Context extends ContextConstraint, +> = JsonRpcMiddleware, Result, Context> | JsonPrimitive; /** * A record of RPC method handler functions or hard-coded results, keyed to particular method names. * Only primitive JSON values are permitted as hard-coded results. */ -export type MiddlewareScaffold = Record< - string, - ScaffoldMiddlewareHandler ->; +export type MiddlewareScaffold< + Context extends ContextConstraint = MiddlewareContext, +> = Record>; /** * Creates a middleware function from an object of RPC method handler functions, @@ -28,9 +37,9 @@ export type MiddlewareScaffold = Record< * @param handlers - The RPC method handler functions. * @returns The scaffold middleware function. */ -export function createScaffoldMiddleware( - handlers: MiddlewareScaffold, -): JsonRpcMiddleware { +export function createScaffoldMiddleware( + handlers: MiddlewareScaffold, +): JsonRpcMiddleware { return ({ request, context, next }) => { const handlerOrResult = handlers[request.method]; if (handlerOrResult === undefined) { From 511fb6585b2bce53456846146d855597f62132fd Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:57:32 -0800 Subject: [PATCH 12/30] refactor: Fix most lint exceptions --- eslint-warning-thresholds.json | 52 +------------------ eslint.config.mjs | 6 +-- .../src/block-cache.ts | 12 ++--- .../src/block-ref.test.ts | 4 ++ .../test/setupAfterEnv.ts | 10 ++-- .../test/util/helpers.ts | 21 +++++--- 6 files changed, 31 insertions(+), 74 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 266558456ca..2f49f24a9b1 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -154,29 +154,7 @@ "@typescript-eslint/no-explicit-any": 1 }, "packages/eth-json-rpc-middleware/src/block-cache.ts": { - "jsdoc/require-jsdoc": 1, - "no-restricted-syntax": 1 - }, - "packages/eth-json-rpc-middleware/src/block-ref-rewrite.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/block-ref.test.ts": { - "jest/expect-expect": 2 - }, - "packages/eth-json-rpc-middleware/src/block-ref.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/block-tracker-inspector.ts": { - "jsdoc/match-description": 2 - }, - "packages/eth-json-rpc-middleware/src/fetch.test.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/fetch.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/inflight-cache.ts": { - "jsdoc/match-description": 4 + "jsdoc/require-jsdoc": 1 }, "packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts": { "jsdoc/require-jsdoc": 1 @@ -187,43 +165,15 @@ "packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts": { "jsdoc/require-jsdoc": 2 }, - "packages/eth-json-rpc-middleware/src/retryOnEmpty.test.ts": { - "jsdoc/match-description": 3 - }, - "packages/eth-json-rpc-middleware/src/retryOnEmpty.ts": { - "jsdoc/match-description": 2 - }, - "packages/eth-json-rpc-middleware/src/utils/cache.ts": { - "jsdoc/match-description": 6 - }, - "packages/eth-json-rpc-middleware/src/utils/common.ts": { - "jsdoc/match-description": 1 - }, "packages/eth-json-rpc-middleware/src/utils/error.ts": { "jsdoc/require-jsdoc": 1 }, - "packages/eth-json-rpc-middleware/src/utils/normalize.ts": { - "jsdoc/match-description": 3 - }, - "packages/eth-json-rpc-middleware/src/utils/timeout.ts": { - "jsdoc/match-description": 1 - }, "packages/eth-json-rpc-middleware/src/utils/validation.ts": { "jsdoc/require-jsdoc": 4 }, "packages/eth-json-rpc-middleware/src/wallet.ts": { - "@typescript-eslint/prefer-nullish-coalescing": 5, - "jsdoc/match-description": 3, "jsdoc/require-jsdoc": 12 }, - "packages/eth-json-rpc-middleware/test/setupAfterEnv.ts": { - "@typescript-eslint/no-explicit-any": 3, - "jsdoc/match-description": 2 - }, - "packages/eth-json-rpc-middleware/test/util/helpers.ts": { - "@typescript-eslint/no-explicit-any": 5, - "jsdoc/match-description": 10 - }, "packages/gas-fee-controller/src/GasFeeController.test.ts": { "import-x/namespace": 2, "import-x/order": 1 diff --git a/eslint.config.mjs b/eslint.config.mjs index 3dd5e68d204..4fccbc52d16 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -234,12 +234,8 @@ const config = createConfig([ { files: ['packages/eth-json-rpc-middleware/**/*.ts'], rules: { - // TODO: Re-enable these rules or add inline ignores for warranted cases - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/prefer-nullish-coalescing': 'warn', - 'jsdoc/match-description': 'warn', + // TODO: Re-enable this 'jsdoc/require-jsdoc': 'warn', - 'no-restricted-syntax': 'warn', }, }, { diff --git a/packages/eth-json-rpc-middleware/src/block-cache.ts b/packages/eth-json-rpc-middleware/src/block-cache.ts index 0c92d8c2a7b..0913527eb55 100644 --- a/packages/eth-json-rpc-middleware/src/block-cache.ts +++ b/packages/eth-json-rpc-middleware/src/block-cache.ts @@ -33,19 +33,19 @@ type BlockCacheMiddlewareOptions = { // class BlockCacheStrategy { - private cache: Cache; + #cache: Cache; constructor() { - this.cache = {}; + this.#cache = {}; } getBlockCache(blockNumberHex: string): BlockCache { const blockNumber: number = Number.parseInt(blockNumberHex, 16); - let blockCache: BlockCache = this.cache[blockNumber]; + let blockCache: BlockCache = this.#cache[blockNumber]; // create new cache if necesary if (!blockCache) { const newCache: BlockCache = {}; - this.cache[blockNumber] = newCache; + this.#cache[blockNumber] = newCache; blockCache = newCache; } return blockCache; @@ -126,10 +126,10 @@ class BlockCacheStrategy { clearBefore(oldBlockHex: string): void { const oldBlockNumber: number = Number.parseInt(oldBlockHex, 16); // clear old caches - Object.keys(this.cache) + Object.keys(this.#cache) .map(Number) .filter((num) => num < oldBlockNumber) - .forEach((num) => delete this.cache[num]); + .forEach((num) => delete this.#cache[num]); } } diff --git a/packages/eth-json-rpc-middleware/src/block-ref.test.ts b/packages/eth-json-rpc-middleware/src/block-ref.test.ts index ef296ecb9a2..e5f5406c546 100644 --- a/packages/eth-json-rpc-middleware/src/block-ref.test.ts +++ b/packages/eth-json-rpc-middleware/src/block-ref.test.ts @@ -196,6 +196,8 @@ describe('createBlockRefMiddleware', () => { describe.each(['earliest', 'pending', '0x200'])( 'if the block param is something other than "latest", like %o', (blockParam) => { + // Using custom expect helper + // eslint-disable-next-line jest/expect-expect it('does not make a direct request through the provider', async () => { const finalMiddleware = createFinalMiddlewareWithDefaultResult(); @@ -267,6 +269,8 @@ describe('createBlockRefMiddleware', () => { }); describe('when the RPC method does not take a block parameter', () => { + // Using custom expect helper + // eslint-disable-next-line jest/expect-expect it('does not make a direct request through the provider', async () => { const finalMiddleware = createFinalMiddlewareWithDefaultResult(); diff --git a/packages/eth-json-rpc-middleware/test/setupAfterEnv.ts b/packages/eth-json-rpc-middleware/test/setupAfterEnv.ts index 7608e43f298..9147c2fdf17 100644 --- a/packages/eth-json-rpc-middleware/test/setupAfterEnv.ts +++ b/packages/eth-json-rpc-middleware/test/setupAfterEnv.ts @@ -37,7 +37,7 @@ expect.extend({ * @param promise - The promise to test. * @returns The result of the matcher. */ - async toNeverResolve(promise: Promise) { + async toNeverResolve(promise: Promise) { if (this.isNot) { throw new Error( 'Using `.not.toNeverResolve(...)` is not supported. ' + @@ -46,8 +46,8 @@ expect.extend({ ); } - let resolutionValue: any; - let rejectionValue: any; + let resolutionValue: unknown; + let rejectionValue: unknown; try { resolutionValue = await Promise.race([ promise, @@ -67,8 +67,8 @@ expect.extend({ message: () => { return `Expected promise to never resolve after ${TIME_TO_WAIT_UNTIL_UNRESOLVED}ms, but it ${ rejectionValue - ? `was rejected with ${rejectionValue}` - : `resolved with ${resolutionValue}` + ? `was rejected with ${JSON.stringify(rejectionValue, null, 2)}` + : `resolved with ${JSON.stringify(resolutionValue, null, 2)}` }`; }, pass: false, diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index cbadde4daee..fa5767e6ace 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -2,7 +2,10 @@ import { PollingBlockTracker } from '@metamask/eth-block-tracker'; import { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { + JsonRpcMiddleware, + ResultConstraint, +} from '@metamask/json-rpc-engine/v2'; import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona/full'; import { isDeepStrictEqual } from 'util'; @@ -113,6 +116,10 @@ export function createProviderAndBlockTracker(): { return { provider, blockTracker }; } +// An expedient for use with createEngine below. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyMiddleware = JsonRpcMiddleware, any>; + /** * Creates a JSON-RPC engine with the middleware under test and any * additional middleware. If no other middleware is provided, a final middleware @@ -123,8 +130,8 @@ export function createProviderAndBlockTracker(): { * @returns The created engine. */ export function createEngine( - middlewareUnderTest: JsonRpcMiddleware, - ...otherMiddleware: JsonRpcMiddleware[] + middlewareUnderTest: AnyMiddleware, + ...otherMiddleware: AnyMiddleware[] ): JsonRpcEngineV2 { return JsonRpcEngineV2.create({ middleware: [ @@ -258,10 +265,10 @@ export function expectProviderRequestNotToHaveBeenMade( * @returns The Jest spy object that represents `provider.request` (so that * you can make assertions on the method later, if you like). */ -export function stubProviderRequests( - provider: InternalProvider, - stubs: ProviderRequestStub[], -) { +export function stubProviderRequests< + Params extends JsonRpcParams = JsonRpcParams, + Result extends Json = Json, +>(provider: InternalProvider, stubs: ProviderRequestStub[]) { const remainingStubs = klona(stubs); const callNumbersByRequest = new Map, number>(); return jest.spyOn(provider, 'request').mockImplementation(async (request) => { From 97db3ae394f0993ded3e53c79cdfbe4c2e18c099 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:50:46 -0800 Subject: [PATCH 13/30] refactor: Fix require-jsdoc violations --- eslint-warning-thresholds.json | 21 ---- eslint.config.mjs | 7 -- .../src/block-cache.ts | 7 ++ .../wallet-request-execution-permissions.ts | 9 ++ .../wallet-revoke-execution-permission.ts | 9 ++ .../src/providerAsMiddleware.ts | 13 +++ .../src/utils/error.ts | 7 ++ .../src/utils/validation.ts | 32 ++++++ .../eth-json-rpc-middleware/src/wallet.ts | 104 ++++++++++++++++++ .../test/util/helpers.ts | 1 - 10 files changed, 181 insertions(+), 29 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 2f49f24a9b1..161800dd2ba 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -153,27 +153,6 @@ "packages/eth-block-tracker/tests/withBlockTracker.ts": { "@typescript-eslint/no-explicit-any": 1 }, - "packages/eth-json-rpc-middleware/src/block-cache.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts": { - "jsdoc/require-jsdoc": 2 - }, - "packages/eth-json-rpc-middleware/src/utils/error.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/utils/validation.ts": { - "jsdoc/require-jsdoc": 4 - }, - "packages/eth-json-rpc-middleware/src/wallet.ts": { - "jsdoc/require-jsdoc": 12 - }, "packages/gas-fee-controller/src/GasFeeController.test.ts": { "import-x/namespace": 2, "import-x/order": 1 diff --git a/eslint.config.mjs b/eslint.config.mjs index 4fccbc52d16..a8fb4fc62dc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -231,13 +231,6 @@ const config = createConfig([ '@typescript-eslint/consistent-type-definitions': 'warn', }, }, - { - files: ['packages/eth-json-rpc-middleware/**/*.ts'], - rules: { - // TODO: Re-enable this - 'jsdoc/require-jsdoc': 'warn', - }, - }, { files: ['packages/foundryup/**/*.{js,ts}'], rules: { diff --git a/packages/eth-json-rpc-middleware/src/block-cache.ts b/packages/eth-json-rpc-middleware/src/block-cache.ts index 0913527eb55..da788362d6e 100644 --- a/packages/eth-json-rpc-middleware/src/block-cache.ts +++ b/packages/eth-json-rpc-middleware/src/block-cache.ts @@ -133,6 +133,13 @@ class BlockCacheStrategy { } } +/** + * Creates a middleware that caches block-related requests. + * + * @param options - The options for the middleware. + * @param options.blockTracker - The block tracker to use. + * @returns The block cache middleware. + */ export function createBlockCacheMiddleware({ blockTracker, }: BlockCacheMiddlewareOptions = {}): JsonRpcMiddleware< diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts index 818da8be660..3ef8acfec99 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts @@ -67,6 +67,15 @@ export type ProcessRequestExecutionPermissionsHook = ( req: JsonRpcRequest, ) => Promise; +/** + * Creates a handler for the `wallet_requestExecutionPermissions` JSON-RPC method. + * + * @param options - The options for the handler. + * @param options.processRequestExecutionPermissions - The function to process the + * request execution permissions request. + * @returns A JSON-RPC middleware function that handles the + * `wallet_requestExecutionPermissions` JSON-RPC method. + */ export function createWalletRequestExecutionPermissionsHandler({ processRequestExecutionPermissions, }: { diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts index 274d5e031a2..5cf054646a5 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts @@ -27,6 +27,15 @@ export type ProcessRevokeExecutionPermissionHook = ( req: JsonRpcRequest, ) => Promise; +/** + * Creates a handler for the `wallet_revokeExecutionPermission` JSON-RPC method. + * + * @param options - The options for the handler. + * @param options.processRevokeExecutionPermission - The function to process the + * revoke execution permission request. + * @returns A JSON-RPC middleware function that handles the + * `wallet_revokeExecutionPermission` JSON-RPC method. + */ export function createWalletRevokeExecutionPermissionHandler({ processRevokeExecutionPermission, }: { diff --git a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts index 8e5edab8f64..2ce4ade7e31 100644 --- a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts @@ -6,6 +6,13 @@ import { import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +/** + * Creates a legacy JSON-RPC middleware that forwards requests to a provider. + * + * @param provider - The provider to forward requests to. + * @returns A legacy JSON-RPC middleware that forwards requests to the provider. + * @deprecated Use {@link providerAsMiddlewareV2} instead. + */ export function providerAsMiddleware( provider: InternalProvider, ): LegacyJsonRpcMiddleware { @@ -14,6 +21,12 @@ export function providerAsMiddleware( }); } +/** + * Creates a V2 JSON-RPC middleware that forwards requests to a provider. + * + * @param provider - The provider to forward requests to. + * @returns A V2 JSON-RPC middleware that forwards requests to the provider. + */ export function providerAsMiddlewareV2( provider: InternalProvider, ): JsonRpcMiddleware { diff --git a/packages/eth-json-rpc-middleware/src/utils/error.ts b/packages/eth-json-rpc-middleware/src/utils/error.ts index dbc693c0bf4..10da9b81163 100644 --- a/packages/eth-json-rpc-middleware/src/utils/error.ts +++ b/packages/eth-json-rpc-middleware/src/utils/error.ts @@ -2,6 +2,13 @@ import { errorCodes } from '@metamask/rpc-errors'; import { isJsonRpcError } from '@metamask/utils'; import type { JsonRpcError } from '@metamask/utils'; +/** + * Checks if a value is a JSON-RPC error that indicates an execution reverted error. + * + * @param error - The value to check. + * @returns True if the value is a JSON-RPC error that indicates an execution reverted + * error, false otherwise. + */ export function isExecutionRevertedError( error: unknown, ): error is JsonRpcError { diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.ts b/packages/eth-json-rpc-middleware/src/utils/validation.ts index d4f85096198..1f4934171c8 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.ts @@ -4,6 +4,17 @@ import type { Struct, StructError } from '@metamask/superstruct'; import { validate } from '@metamask/superstruct'; import type { Hex } from '@metamask/utils'; +/** + * Validates and normalizes a keyholder address for transaction- and + * signature-related operations. + * + * @param address - The Ethereum address to validate and normalize. + * @param context - The context of the request. + * @param options - The options for the validation. + * @param options.getAccounts - The function to get the accounts for the origin. + * @returns The normalized address, if valid. Otherwise, throws + * an error + */ export async function validateAndNormalizeKeyholder( address: Hex, context: MiddlewareContext<{ origin: string }>, @@ -36,6 +47,14 @@ export async function validateAndNormalizeKeyholder( }); } +/** + * Validates the parameters of a request against a Superstruct schema. + * Throws a JSON-RPC error if the parameters are invalid. + * + * @param value - The value to validate. + * @param struct - The Superstruct schema to validate against. + * @throws An error if the parameters are invalid. + */ export function validateParams( value: unknown | ParamsType, struct: Struct, @@ -49,11 +68,24 @@ export function validateParams( } } +/** + * Checks if a string resembles an Ethereum address. + * + * @param str - The string to check. + * @returns True if the string resembles an Ethereum address, false otherwise. + */ export function resemblesAddress(str: string): boolean { // hex prefix 2 + 20 bytes return str.length === 2 + 20 * 2; } +/** + * Formats a Superstruct validation error into a human-readable string. + * + * @param error - The Superstruct validation error. + * @param message - The base error message to prepend to the formatted details. + * @returns The formatted error. + */ function formatValidationError(error: StructError, message: string): string { return `${message}\n\n${error .failures() diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index 0c12fb7658a..ac5e1b5789c 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -88,6 +88,25 @@ export type WalletMiddlewareParams = MiddlewareParams< WalletMiddlewareContext >; +/** + * Creates a JSON-RPC middleware that handles "wallet"-related JSON-RPC methods. + * "Wallet" may have had a specific meaning at some point in the distant past, + * but at this point it's just an arbitrary label. + * + * @param options - The options for the middleware. + * @param options.getAccounts - The function to get the accounts for the origin. + * @param options.processDecryptMessage - The function to process the decrypt message request. + * @param options.processEncryptionPublicKey - The function to process the encryption public key request. + * @param options.processPersonalMessage - The function to process the personal message request. + * @param options.processTransaction - The function to process the transaction request. + * @param options.processSignTransaction - The function to process the sign transaction request. + * @param options.processTypedMessage - The function to process the typed message request. + * @param options.processTypedMessageV3 - The function to process the typed message v3 request. + * @param options.processTypedMessageV4 - The function to process the typed message v4 request. + * @param options.processRequestExecutionPermissions - The function to process the request execution permissions request. + * @param options.processRevokeExecutionPermission - The function to process the revoke execution permission request. + * @returns A JSON-RPC middleware that handles wallet-related JSON-RPC methods. + */ export function createWalletMiddleware({ getAccounts, processDecryptMessage, @@ -142,12 +161,26 @@ export function createWalletMiddleware({ // account lookups // + /** + * Gets the accounts for the origin. + * + * @param options - Options bag. + * @param options.context - The context of the request. + * @returns The accounts for the origin. + */ async function lookupAccounts({ context, }: WalletMiddlewareParams): Promise { return await getAccounts(context.assertGet('origin')); } + /** + * Gets the default account (i.e. first in the list) for the origin. + * + * @param options - Options bag. + * @param options.context - The context of the request. + * @returns The default account for the origin. + */ async function lookupDefaultAccount({ context, }: WalletMiddlewareParams): Promise { @@ -159,6 +192,14 @@ export function createWalletMiddleware({ // transaction signatures // + /** + * Sends a transaction. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The transaction hash. + */ async function sendTransaction({ request, context, @@ -182,6 +223,14 @@ export function createWalletMiddleware({ return await processTransaction(txParams, request); } + /** + * Signs a transaction. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed transaction. + */ async function signTransaction({ request, context, @@ -209,6 +258,14 @@ export function createWalletMiddleware({ // message signatures // + /** + * Signs a `eth_signTypedData` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed message. + */ async function signTypedData({ request, context, @@ -244,6 +301,14 @@ export function createWalletMiddleware({ return await processTypedMessage(msgParams, request, version); } + /** + * Signs a `eth_signTypedData_v3` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed message. + */ async function signTypedDataV3({ request, context, @@ -276,6 +341,14 @@ export function createWalletMiddleware({ return await processTypedMessageV3(msgParams, request, version); } + /** + * Signs a `eth_signTypedData_v4` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed message. + */ async function signTypedDataV4({ request, context, @@ -308,6 +381,14 @@ export function createWalletMiddleware({ return await processTypedMessageV4(msgParams, request, version); } + /** + * Signs a `personal_sign` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed message. + */ async function personalSign({ request, context, @@ -358,6 +439,13 @@ export function createWalletMiddleware({ return await processPersonalMessage(msgParams, request); } + /** + * Recovers the signer address from a `personal_sign` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @returns The recovered signer address. + */ async function personalRecover({ request, }: WalletMiddlewareParams): Promise { @@ -380,6 +468,14 @@ export function createWalletMiddleware({ return signerAddress; } + /** + * Gets the encryption public key for an address. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The encryption public key. + */ async function encryptionPublicKey({ request, context, @@ -402,6 +498,14 @@ export function createWalletMiddleware({ return await processEncryptionPublicKey(address, request); } + /** + * Decrypts a message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The decrypted message. + */ async function decryptMessage({ request, context, diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index fa5767e6ace..630bfc43d08 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -20,7 +20,6 @@ export const createRequest = < jsonrpc: '2.0', id: request.id ?? '1', method: request.method ?? 'test_request', - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing params: request.params === undefined ? [] : request.params, } as Output; }; From 928ada8cf95abc9133f1af9d92bb05722c1b62bf Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 10:55:49 -0800 Subject: [PATCH 14/30] refactor: Migrate processTransaction --- .../src/utils/validation.test.ts | 3 ++- .../src/utils/validation.ts | 5 +++-- .../eth-json-rpc-middleware/src/wallet.ts | 14 +++++++++++-- .../test/util/helpers.ts | 21 ++++++++++++------- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts index 903553453c4..c833e4e1d9e 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts @@ -8,6 +8,7 @@ import { validateAndNormalizeKeyholder, validateParams, } from './validation'; +import type { WalletMiddlewareKeyValues } from '../wallet'; jest.mock('@metamask/superstruct', () => ({ ...jest.requireActual('@metamask/superstruct'), @@ -16,7 +17,7 @@ jest.mock('@metamask/superstruct', () => ({ const ADDRESS_MOCK = '0xABCDabcdABCDabcdABCDabcdABCDabcdABCDabcd'; const createContext = () => - new MiddlewareContext<{ origin: string }>([['origin', 'test']]); + new MiddlewareContext([['origin', 'test']]); const STRUCT_ERROR_MOCK = { failures: () => [ diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.ts b/packages/eth-json-rpc-middleware/src/utils/validation.ts index 1f4934171c8..9af85cd8a48 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.ts @@ -1,9 +1,10 @@ -import type { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { Struct, StructError } from '@metamask/superstruct'; import { validate } from '@metamask/superstruct'; import type { Hex } from '@metamask/utils'; +import type { WalletMiddlewareContext } from '../wallet'; + /** * Validates and normalizes a keyholder address for transaction- and * signature-related operations. @@ -17,7 +18,7 @@ import type { Hex } from '@metamask/utils'; */ export async function validateAndNormalizeKeyholder( address: Hex, - context: MiddlewareContext<{ origin: string }>, + context: WalletMiddlewareContext, { getAccounts }: { getAccounts: (origin: string) => Promise }, ): Promise { if ( diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index ac5e1b5789c..b6d9f89e2f5 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -58,6 +58,7 @@ export type WalletMiddlewareOptions = { processTransaction?: ( txParams: TransactionParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, ) => Promise; processSignTransaction?: ( txParams: TransactionParams, @@ -82,7 +83,16 @@ export type WalletMiddlewareOptions = { processRevokeExecutionPermission?: ProcessRevokeExecutionPermissionHook; }; -export type WalletMiddlewareContext = MiddlewareContext<{ origin: string }>; +export type WalletMiddlewareKeyValues = { + networkClientId: string; + origin: string; + securityAlertResponse?: Record; + traceContext?: unknown; +}; + +export type WalletMiddlewareContext = + MiddlewareContext; + export type WalletMiddlewareParams = MiddlewareParams< JsonRpcRequest, WalletMiddlewareContext @@ -220,7 +230,7 @@ export function createWalletMiddleware({ ...params, from: await validateAndNormalizeKeyholder(params?.from || '', context), }; - return await processTransaction(txParams, request); + return await processTransaction(txParams, request, context); } /** diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index 630bfc43d08..998481f9fdf 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -10,6 +10,8 @@ import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona/full'; import { isDeepStrictEqual } from 'util'; +import type { WalletMiddlewareKeyValues } from '../../src/wallet'; + export const createRequest = < Input extends Partial>, Output extends Input & JsonRpcRequest, @@ -24,20 +26,25 @@ export const createRequest = < } as Output; }; -const createHandleOptions = () => ({ +const createHandleOptions = ( + keyValues: Partial = {}, +): { context: WalletMiddlewareKeyValues } => ({ context: { - origin: 'test', + networkClientId: 'test-client-id', + origin: 'test-origin', + ...keyValues, }, }); export const createHandleParams = < - Input extends Partial>, - Output extends Input & JsonRpcRequest, + InputReq extends Partial>, + OutputReq extends InputReq & JsonRpcRequest, >( - request: Input, -): [Output, ReturnType] => [ + request: InputReq, + keyValues: Partial = {}, +): [OutputReq, ReturnType] => [ createRequest(request), - createHandleOptions(), + createHandleOptions(keyValues), ]; /** From b83331271226184aa418b11bcf571b56996df4e3 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:23:05 -0800 Subject: [PATCH 15/30] refactor: Migrate process encrypt / decrypt message --- eslint-warning-thresholds.json | 2 +- packages/eth-json-rpc-middleware/package.json | 1 + packages/eth-json-rpc-middleware/src/wallet.ts | 17 +++++++++++++---- .../eth-json-rpc-middleware/tsconfig.build.json | 3 +++ packages/eth-json-rpc-middleware/tsconfig.json | 3 +++ .../src/AbstractMessageManager.test.ts | 6 +++--- .../src/AbstractMessageManager.ts | 17 +++++++---------- .../src/DecryptMessageManager.ts | 6 +++--- .../src/EncryptionPublicKeyManager.ts | 6 +++--- yarn.lock | 3 ++- 10 files changed, 39 insertions(+), 25 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 161800dd2ba..5e6bcb7a90e 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -194,7 +194,7 @@ "jest/no-conditional-in-test": 7 }, "packages/message-manager/src/AbstractMessageManager.ts": { - "jsdoc/check-tag-names": 25, + "jsdoc/check-tag-names": 23, "jsdoc/tag-lines": 2 }, "packages/message-manager/src/DecryptMessageManager.test.ts": { diff --git a/packages/eth-json-rpc-middleware/package.json b/packages/eth-json-rpc-middleware/package.json index 1b8a8e63aaf..07c26870e21 100644 --- a/packages/eth-json-rpc-middleware/package.json +++ b/packages/eth-json-rpc-middleware/package.json @@ -59,6 +59,7 @@ "@metamask/eth-json-rpc-provider": "^5.0.1", "@metamask/eth-sig-util": "^8.2.0", "@metamask/json-rpc-engine": "^10.1.1", + "@metamask/message-manager": "^14.0.0", "@metamask/rpc-errors": "^7.0.2", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.8.1", diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index b6d9f89e2f5..2d01b868666 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -5,6 +5,7 @@ import type { MiddlewareParams, } from '@metamask/json-rpc-engine/v2'; import { createScaffoldMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { MessageRequest } from '@metamask/message-manager'; import { rpcErrors } from '@metamask/rpc-errors'; import { isValidHexAddress } from '@metamask/utils'; import type { JsonRpcRequest, Json, Hex } from '@metamask/utils'; @@ -45,11 +46,11 @@ export type WalletMiddlewareOptions = { getAccounts: (origin: string) => Promise; processDecryptMessage?: ( msgParams: MessageParams, - req: JsonRpcRequest, + req: MessageRequest, ) => Promise; processEncryptionPublicKey?: ( address: string, - req: JsonRpcRequest, + req: MessageRequest, ) => Promise; processPersonalMessage?: ( msgParams: MessageParams, @@ -505,7 +506,11 @@ export function createWalletMiddleware({ const address = await validateAndNormalizeKeyholder(params[0], context); - return await processEncryptionPublicKey(address, request); + return await processEncryptionPublicKey(address, { + id: request.id as string | number, + origin: context.assertGet('origin'), + securityAlertResponse: context.assertGet('securityAlertResponse'), + }); } /** @@ -544,7 +549,11 @@ export function createWalletMiddleware({ data: ciphertext, }; - return await processDecryptMessage(msgParams, request); + return await processDecryptMessage(msgParams, { + id: request.id as string | number, + origin: context.assertGet('origin'), + securityAlertResponse: context.assertGet('securityAlertResponse'), + }); } // diff --git a/packages/eth-json-rpc-middleware/tsconfig.build.json b/packages/eth-json-rpc-middleware/tsconfig.build.json index f3452fcd990..8aacbb0393f 100644 --- a/packages/eth-json-rpc-middleware/tsconfig.build.json +++ b/packages/eth-json-rpc-middleware/tsconfig.build.json @@ -17,6 +17,9 @@ }, { "path": "../json-rpc-engine/tsconfig.build.json" + }, + { + "path": "../message-manager/tsconfig.build.json" } ], "include": ["../../types", "./src"], diff --git a/packages/eth-json-rpc-middleware/tsconfig.json b/packages/eth-json-rpc-middleware/tsconfig.json index 1788c61e4fe..240743caf93 100644 --- a/packages/eth-json-rpc-middleware/tsconfig.json +++ b/packages/eth-json-rpc-middleware/tsconfig.json @@ -18,6 +18,9 @@ }, { "path": "../network-controller" + }, + { + "path": "../message-manager" } ], "include": ["../../types", "./src", "./test"] diff --git a/packages/message-manager/src/AbstractMessageManager.test.ts b/packages/message-manager/src/AbstractMessageManager.test.ts index ee696f53bd8..36d178bcb65 100644 --- a/packages/message-manager/src/AbstractMessageManager.test.ts +++ b/packages/message-manager/src/AbstractMessageManager.test.ts @@ -11,7 +11,7 @@ import { type AbstractMessage, type AbstractMessageParams, type MessageManagerState, - type OriginalRequest, + type MessageRequest, type SecurityProviderRequest, } from './AbstractMessageManager'; @@ -50,7 +50,7 @@ class AbstractTestManager extends AbstractMessageManager< > { addRequestToMessageParams( messageParams: MessageParams, - req?: OriginalRequest, + req?: MessageRequest, ) { return super.addRequestToMessageParams(messageParams, req); } @@ -58,7 +58,7 @@ class AbstractTestManager extends AbstractMessageManager< createUnapprovedMessage( messageParams: MessageParams, type: ApprovalType, - req?: OriginalRequest, + req?: MessageRequest, ) { return super.createUnapprovedMessage(messageParams, type, req); } diff --git a/packages/message-manager/src/AbstractMessageManager.ts b/packages/message-manager/src/AbstractMessageManager.ts index d820246136e..eefe259f410 100644 --- a/packages/message-manager/src/AbstractMessageManager.ts +++ b/packages/message-manager/src/AbstractMessageManager.ts @@ -37,13 +37,10 @@ const getDefaultState = () => ({ }); /** - * @type OriginalRequest - * - * Represents the original request object for adding a message. - * @property origin? - Is it is specified, represents the origin + * Represents the request adding a message. */ -export type OriginalRequest = { - id?: number; +export type MessageRequest = { + id?: string | number; origin?: string; securityAlertResponse?: Record; }; @@ -83,7 +80,7 @@ export type AbstractMessage = { export type AbstractMessageParams = { from: string; origin?: string; - requestId?: number; + requestId?: string | number; deferSetAsSigned?: boolean; }; @@ -211,7 +208,7 @@ export abstract class AbstractMessageManager< */ protected addRequestToMessageParams< MessageParams extends AbstractMessageParams, - >(messageParams: MessageParams, req?: OriginalRequest) { + >(messageParams: MessageParams, req?: MessageRequest) { const updatedMessageParams = { ...messageParams, }; @@ -233,7 +230,7 @@ export abstract class AbstractMessageManager< */ protected createUnapprovedMessage< MessageParams extends AbstractMessageParams, - >(messageParams: MessageParams, type: ApprovalType, req?: OriginalRequest) { + >(messageParams: MessageParams, type: ApprovalType, req?: MessageRequest) { const messageId = random(); return { @@ -519,7 +516,7 @@ export abstract class AbstractMessageManager< */ abstract addUnapprovedMessage( messageParams: ParamsMetamask, - request: OriginalRequest, + request: MessageRequest, version?: string, ): Promise; diff --git a/packages/message-manager/src/DecryptMessageManager.ts b/packages/message-manager/src/DecryptMessageManager.ts index 0ae8d1d80bd..75ab4349c4a 100644 --- a/packages/message-manager/src/DecryptMessageManager.ts +++ b/packages/message-manager/src/DecryptMessageManager.ts @@ -11,7 +11,7 @@ import type { AbstractMessageParams, AbstractMessageParamsMetamask, MessageManagerState, - OriginalRequest, + MessageRequest, SecurityProviderRequest, } from './AbstractMessageManager'; import { AbstractMessageManager } from './AbstractMessageManager'; @@ -131,7 +131,7 @@ export class DecryptMessageManager extends AbstractMessageManager< */ async addUnapprovedMessageAsync( messageParams: DecryptMessageParams, - req?: OriginalRequest, + req?: MessageRequest, ): Promise { validateDecryptedMessageData(messageParams); const messageId = await this.addUnapprovedMessage(messageParams, req); @@ -181,7 +181,7 @@ export class DecryptMessageManager extends AbstractMessageManager< */ async addUnapprovedMessage( messageParams: DecryptMessageParams, - req?: OriginalRequest, + req?: MessageRequest, ) { const updatedMessageParams = this.addRequestToMessageParams( messageParams, diff --git a/packages/message-manager/src/EncryptionPublicKeyManager.ts b/packages/message-manager/src/EncryptionPublicKeyManager.ts index eff493ee3fb..e41b74b46d7 100644 --- a/packages/message-manager/src/EncryptionPublicKeyManager.ts +++ b/packages/message-manager/src/EncryptionPublicKeyManager.ts @@ -10,7 +10,7 @@ import type { AbstractMessageParams, AbstractMessageParamsMetamask, MessageManagerState, - OriginalRequest, + MessageRequest, SecurityProviderRequest, } from './AbstractMessageManager'; import { AbstractMessageManager } from './AbstractMessageManager'; @@ -131,7 +131,7 @@ export class EncryptionPublicKeyManager extends AbstractMessageManager< */ async addUnapprovedMessageAsync( messageParams: EncryptionPublicKeyParams, - req?: OriginalRequest, + req?: MessageRequest, ): Promise { validateEncryptionPublicKeyMessageData(messageParams); const messageId = await this.addUnapprovedMessage(messageParams, req); @@ -175,7 +175,7 @@ export class EncryptionPublicKeyManager extends AbstractMessageManager< */ async addUnapprovedMessage( messageParams: EncryptionPublicKeyParams, - req?: OriginalRequest, + req?: MessageRequest, ): Promise { const updatedMessageParams = this.addRequestToMessageParams( messageParams, diff --git a/yarn.lock b/yarn.lock index 5bf138de969..e8ed70a0fad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3598,6 +3598,7 @@ __metadata: "@metamask/eth-json-rpc-provider": "npm:^5.0.1" "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/json-rpc-engine": "npm:^10.1.1" + "@metamask/message-manager": "npm:^14.0.0" "@metamask/network-controller": "npm:^25.0.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/superstruct": "npm:^3.1.0" @@ -4139,7 +4140,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/message-manager@workspace:packages/message-manager": +"@metamask/message-manager@npm:^14.0.0, @metamask/message-manager@workspace:packages/message-manager": version: 0.0.0-use.local resolution: "@metamask/message-manager@workspace:packages/message-manager" dependencies: From 536ed51cb61f4087aea59940142eab49636f38e8 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:16:43 -0700 Subject: [PATCH 16/30] docs: Update changelogs --- packages/eth-json-rpc-middleware/CHANGELOG.md | 3 +++ packages/json-rpc-engine/CHANGELOG.md | 2 +- packages/message-manager/CHANGELOG.md | 5 +++++ packages/network-controller/CHANGELOG.md | 2 ++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index 673180d1897..5dc3ad3053f 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Migrate to `JsonRpcEngineV2` ([#7065](https://github.com/MetaMask/core/pull/7065)) + - Migrates all middleware from `JsonRpcEngine` to `JsonRpcEngineV2`. + - To continue using this package with the legacy `JsonRpcEngine`, use the `asLegacyMiddleware` backwards compatibility function. - **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)) diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index ea26e0a1b9d..f5daa2b54f3 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 -- 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), [#7032](https://github.com/MetaMask/core/pull/7032), [#7001](https://github.com/MetaMask/core/pull/7001), [#7061](https://github.com/MetaMask/core/pull/7061)) +- 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), [#7032](https://github.com/MetaMask/core/pull/7032), [#7001](https://github.com/MetaMask/core/pull/7001), [#7061](https://github.com/MetaMask/core/pull/7061), [#7065](https://github.com/MetaMask/core/pull/7065)) - This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation. See the readme for details. ## [10.1.1] diff --git a/packages/message-manager/CHANGELOG.md b/packages/message-manager/CHANGELOG.md index fe2ff35baee..ece5141f53f 100644 --- a/packages/message-manager/CHANGELOG.md +++ b/packages/message-manager/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Rename `OriginalRequest` type to `MessageRequest` and permit string `id` values ([#7065](https://github.com/MetaMask/core/pull/7065)) + - Previously, only number values were permitted for the `id` property. + ## [14.0.0] ### Changed diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 518ae057ea3..754630d8fde 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. +- **BREAKING:** Migrate `NetworkClient` to `JsonRpcEngineV2` ([#7065](https://github.com/MetaMask/core/pull/7065)) + - This ought to be unobservable, but we mark it as breaking out of an abundance of caution. - Bump `@metamask/controller-utils` from `^11.14.1` to `^11.15.0` ([#7003](https://github.com/MetaMask/core/pull/7003)) ### Fixed From 03969c699980bbc245854701b5d8e233a291bdb5 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:21:26 -0800 Subject: [PATCH 17/30] refactor: Migrate process signature functions --- packages/eth-json-rpc-middleware/src/wallet.ts | 15 ++++++++++----- .../tests/NetworkController.test.ts | 1 - packages/signature-controller/src/types.ts | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index 2d01b868666..05a0734c633 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -55,6 +55,7 @@ export type WalletMiddlewareOptions = { processPersonalMessage?: ( msgParams: MessageParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, ) => Promise; processTransaction?: ( txParams: TransactionParams, @@ -64,20 +65,24 @@ export type WalletMiddlewareOptions = { processSignTransaction?: ( txParams: TransactionParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, ) => Promise; processTypedMessage?: ( msgParams: TypedMessageV1Params, req: JsonRpcRequest, + context: WalletMiddlewareContext, version: string, ) => Promise; processTypedMessageV3?: ( msgParams: TypedMessageParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, version: string, ) => Promise; processTypedMessageV4?: ( msgParams: TypedMessageParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, version: string, ) => Promise; processRequestExecutionPermissions?: ProcessRequestExecutionPermissionsHook; @@ -262,7 +267,7 @@ export function createWalletMiddleware({ ...params, from: await validateAndNormalizeKeyholder(params?.from || '', context), }; - return await processSignTransaction(txParams, request); + return await processSignTransaction(txParams, request, context); } // @@ -309,7 +314,7 @@ export function createWalletMiddleware({ version, }; - return await processTypedMessage(msgParams, request, version); + return await processTypedMessage(msgParams, request, context, version); } /** @@ -349,7 +354,7 @@ export function createWalletMiddleware({ signatureMethod: 'eth_signTypedData_v3', }; - return await processTypedMessageV3(msgParams, request, version); + return await processTypedMessageV3(msgParams, request, context, version); } /** @@ -389,7 +394,7 @@ export function createWalletMiddleware({ signatureMethod: 'eth_signTypedData_v4', }; - return await processTypedMessageV4(msgParams, request, version); + return await processTypedMessageV4(msgParams, request, context, version); } /** @@ -447,7 +452,7 @@ export function createWalletMiddleware({ signatureMethod: 'personal_sign', }; - return await processPersonalMessage(msgParams, request); + return await processPersonalMessage(msgParams, request, context); } /** diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 1b7b36da05b..18cbac232a2 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -16804,7 +16804,6 @@ async function waitForPublishedEvents({ if (interestingEventPayloads.length === expectedNumberOfEvents) { resolve(interestingEventPayloads); } else { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject( new Error( `Expected to receive ${expectedNumberOfEvents} ${String(eventType)} event(s), but received ${ diff --git a/packages/signature-controller/src/types.ts b/packages/signature-controller/src/types.ts index cbfa4ca5ba4..a7346b660d2 100644 --- a/packages/signature-controller/src/types.ts +++ b/packages/signature-controller/src/types.ts @@ -27,7 +27,7 @@ export enum DecodingDataChangeType { /** Original client request that triggered the signature request. */ export type OriginalRequest = { /** Unique ID to identify the client request. */ - id?: number; + id?: number | string; /** Method of signature request */ method?: string; From 2045196c6b7f91e29e89d3fd0c1137ceb36bfa4a Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:22:40 -0800 Subject: [PATCH 18/30] docs: Update signature-controller changelog --- packages/signature-controller/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/signature-controller/CHANGELOG.md b/packages/signature-controller/CHANGELOG.md index 9992233752b..09a0738630c 100644 --- a/packages/signature-controller/CHANGELOG.md +++ b/packages/signature-controller/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Permit string `id` values in `OriginalRequest` type ([#7065](https://github.com/MetaMask/core/pull/7065)) + - Previously, only number values were permitted for the `id` property. + ## [36.0.0] ### Changed From d1754980e39e0f84034aea232dca880b5902b7ce Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:24:36 -0800 Subject: [PATCH 19/30] chore: Lint --- packages/signature-controller/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/signature-controller/CHANGELOG.md b/packages/signature-controller/CHANGELOG.md index 09a0738630c..8945e753dd5 100644 --- a/packages/signature-controller/CHANGELOG.md +++ b/packages/signature-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + - Permit string `id` values in `OriginalRequest` type ([#7065](https://github.com/MetaMask/core/pull/7065)) - Previously, only number values were permitted for the `id` property. From 64d897d9bcb6a6428777482b45fdd41df94e164b Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:31:08 -0800 Subject: [PATCH 20/30] refactor: Handle frozen data in normalizeTypedMessage --- packages/eth-json-rpc-middleware/package.json | 2 ++ .../src/utils/normalize.test.ts | 16 ++++++++++++++++ .../src/utils/normalize.ts | 10 +++++++--- yarn.lock | 2 ++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/eth-json-rpc-middleware/package.json b/packages/eth-json-rpc-middleware/package.json index 07c26870e21..0c9f4932118 100644 --- a/packages/eth-json-rpc-middleware/package.json +++ b/packages/eth-json-rpc-middleware/package.json @@ -72,8 +72,10 @@ "@metamask/error-reporting-service": "^3.0.0", "@metamask/network-controller": "^25.0.0", "@ts-bridge/cli": "^0.6.4", + "@types/deep-freeze-strict": "^1.1.0", "@types/jest": "^27.4.1", "@types/pify": "^5.0.2", + "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", "tsd": "^0.31.2", diff --git a/packages/eth-json-rpc-middleware/src/utils/normalize.test.ts b/packages/eth-json-rpc-middleware/src/utils/normalize.test.ts index 98d64913f7e..046d30f8377 100644 --- a/packages/eth-json-rpc-middleware/src/utils/normalize.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/normalize.test.ts @@ -1,3 +1,6 @@ +import deepFreeze from 'deep-freeze-strict'; +import { klona } from 'klona'; + import { normalizeTypedMessage } from './normalize'; const MESSAGE_DATA_MOCK = { @@ -79,6 +82,19 @@ describe('normalizeTypedMessage', () => { ); }); + it('should normalize verifyingContract address in readonly data', () => { + const data = klona(MESSAGE_DATA_MOCK); + data.domain.verifyingContract = + '0Xae7ab96520de3a18e5e111b5eaab095312d7fe84'; + deepFreeze(data); + + const normalizedData = parseNormalizerResult(data); + + expect(normalizedData.domain.verifyingContract).toBe( + '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + ); + }); + it('should not modify if verifyingContract is already hexadecimal', () => { const expectedVerifyingContract = '0xae7ab96520de3a18e5e111b5eaab095312d7fe84'; diff --git a/packages/eth-json-rpc-middleware/src/utils/normalize.ts b/packages/eth-json-rpc-middleware/src/utils/normalize.ts index e7f2982aff5..55b37851891 100644 --- a/packages/eth-json-rpc-middleware/src/utils/normalize.ts +++ b/packages/eth-json-rpc-middleware/src/utils/normalize.ts @@ -32,9 +32,13 @@ export function normalizeTypedMessage(messageData: string) { return messageData; } - data.domain.verifyingContract = normalizeContractAddress(verifyingContract); - - return JSON.stringify(data); + return JSON.stringify({ + ...data, + domain: { + ...data.domain, + verifyingContract: normalizeContractAddress(verifyingContract), + }, + }); } /** diff --git a/yarn.lock b/yarn.lock index e8ed70a0fad..080f7afdb68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3604,8 +3604,10 @@ __metadata: "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.8.1" "@ts-bridge/cli": "npm:^0.6.4" + "@types/deep-freeze-strict": "npm:^1.1.0" "@types/jest": "npm:^27.4.1" "@types/pify": "npm:^5.0.2" + deep-freeze-strict: "npm:^1.1.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" klona: "npm:^2.0.6" From 5a558267d6bd39a0c18bbf23c0b9d7ebdf5cea7d Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:48:44 -0800 Subject: [PATCH 21/30] fix: Do not assertGet securityAlertResponse --- packages/eth-json-rpc-middleware/src/wallet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index 05a0734c633..264e90ec5d0 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -514,7 +514,7 @@ export function createWalletMiddleware({ return await processEncryptionPublicKey(address, { id: request.id as string | number, origin: context.assertGet('origin'), - securityAlertResponse: context.assertGet('securityAlertResponse'), + securityAlertResponse: context.get('securityAlertResponse'), }); } @@ -557,7 +557,7 @@ export function createWalletMiddleware({ return await processDecryptMessage(msgParams, { id: request.id as string | number, origin: context.assertGet('origin'), - securityAlertResponse: context.assertGet('securityAlertResponse'), + securityAlertResponse: context.get('securityAlertResponse'), }); } From 836ca7460412f78a446921c4f8c17f660884b578 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:04:05 -0800 Subject: [PATCH 22/30] refactor: Improve asV2Middleware type --- packages/json-rpc-engine/src/asV2Middleware.ts | 10 ++++++---- .../network-controller/src/create-network-client.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/json-rpc-engine/src/asV2Middleware.ts b/packages/json-rpc-engine/src/asV2Middleware.ts index 14333cbb4e9..e66cd9b02ca 100644 --- a/packages/json-rpc-engine/src/asV2Middleware.ts +++ b/packages/json-rpc-engine/src/asV2Middleware.ts @@ -14,6 +14,7 @@ import type { } from './JsonRpcEngine'; import { type JsonRpcMiddleware as LegacyMiddleware } from './JsonRpcEngine'; import { mergeMiddleware } from './mergeMiddleware'; +import type { ContextConstraint, MiddlewareContext } from './v2'; import { deepClone, fromLegacyRequest, @@ -47,12 +48,13 @@ export function asV2Middleware< * @returns The {@link JsonRpcEngineV2} middleware. */ export function asV2Middleware< - Params extends JsonRpcParams, - Request extends JsonRpcRequest, - Result extends Json, + Params extends JsonRpcParams = JsonRpcParams, + Request extends JsonRpcRequest = JsonRpcRequest, + Result extends ResultConstraint = ResultConstraint, + Context extends ContextConstraint = MiddlewareContext, >( ...middleware: LegacyMiddleware[] -): JsonRpcMiddleware; +): JsonRpcMiddleware; /** * The asV2Middleware implementation. diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 160aa99c6d3..8ae4565b767 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -151,7 +151,7 @@ export function createNetworkClient({ source: 'metamask', }, }), - ) as unknown as RpcApiMiddleware; + ); } else { rpcApiMiddleware = createFetchMiddleware({ rpcService: rpcServiceChain }); } From 8a637470c406e176f4d15d749c9c097138925661 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:57:05 -0800 Subject: [PATCH 23/30] chore: Lint --- eslint-warning-thresholds.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 32806d330b2..b3ab1f0e4c7 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -287,9 +287,6 @@ "packages/name-controller/src/util.ts": { "jsdoc/require-returns": 1 }, - "packages/network-controller/src/NetworkController.ts": { - "@typescript-eslint/no-misused-promises": 1 - }, "packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts": { "@typescript-eslint/no-misused-promises": 1 }, From 92a672c5085a981eecdcde359d612e9d256321d2 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sat, 8 Nov 2025 08:57:28 -0800 Subject: [PATCH 24/30] refactor: Pass original, frozen request to RpcService --- packages/eth-json-rpc-middleware/src/fetch.ts | 3 +- packages/network-controller/package.json | 2 + .../src/rpc-service/rpc-service.test.ts | 37 +++++++++++++++++++ .../src/rpc-service/rpc-service.ts | 5 ++- yarn.lock | 2 + 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/fetch.ts b/packages/eth-json-rpc-middleware/src/fetch.ts index ff6bf2d92b8..63812406aaf 100644 --- a/packages/eth-json-rpc-middleware/src/fetch.ts +++ b/packages/eth-json-rpc-middleware/src/fetch.ts @@ -4,7 +4,6 @@ import type { } from '@metamask/json-rpc-engine/v2'; import { rpcErrors } from '@metamask/rpc-errors'; import type { Json, JsonRpcRequest } from '@metamask/utils'; -import { klona } from 'klona'; import type { AbstractRpcServiceLike } from './types'; @@ -42,7 +41,7 @@ export function createFetchMiddleware({ ? { [options.originHttpHeaderKey]: origin } : {}; - const jsonRpcResponse = await rpcService.request(klona(request), { + const jsonRpcResponse = await rpcService.request(request, { headers, }); diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index ece0f425aaa..a0672a0c503 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -73,10 +73,12 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/error-reporting-service": "^3.0.0", "@ts-bridge/cli": "^0.6.4", + "@types/deep-freeze-strict": "^1.1.0", "@types/jest": "^27.4.1", "@types/jest-when": "^2.7.3", "@types/lodash": "^4.14.191", "@types/node-fetch": "^2.6.12", + "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", "jest-when": "^3.4.2", diff --git a/packages/network-controller/src/rpc-service/rpc-service.test.ts b/packages/network-controller/src/rpc-service/rpc-service.test.ts index 3bd4249d3b7..83e2aeaacec 100644 --- a/packages/network-controller/src/rpc-service/rpc-service.test.ts +++ b/packages/network-controller/src/rpc-service/rpc-service.test.ts @@ -3,6 +3,7 @@ import { HttpError } from '@metamask/controller-utils'; import { errorCodes } from '@metamask/rpc-errors'; +import deepFreeze from 'deep-freeze-strict'; import nock from 'nock'; import { FetchError } from 'node-fetch'; import { useFakeTimers } from 'sinon'; @@ -934,6 +935,42 @@ describe('RpcService', () => { }); }); + it('handles deeply frozen JSON-RPC requests', async () => { + const endpointUrl = 'https://rpc.example.chain'; + nock(endpointUrl) + .post('/', { + id: 1, + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + }) + .reply(200, { + id: 1, + jsonrpc: '2.0', + result: '0x1', + }); + const service = new RpcService({ + fetch, + btoa, + endpointUrl, + }); + + const response = await service.request( + deepFreeze({ + id: 1, + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + }), + ); + + expect(response).toStrictEqual({ + id: 1, + jsonrpc: '2.0', + result: '0x1', + }); + }); + it('does not throw if the endpoint returns an unsuccessful JSON-RPC response', async () => { const endpointUrl = 'https://rpc.example.chain'; nock(endpointUrl) diff --git a/packages/network-controller/src/rpc-service/rpc-service.ts b/packages/network-controller/src/rpc-service/rpc-service.ts index 51f5876fed4..7021e8167cd 100644 --- a/packages/network-controller/src/rpc-service/rpc-service.ts +++ b/packages/network-controller/src/rpc-service/rpc-service.ts @@ -429,7 +429,8 @@ export class RpcService implements AbstractRpcService { ): Promise>; async request( - jsonRpcRequest: JsonRpcRequest, + // The request object may be frozen and must not be mutated. + jsonRpcRequest: Readonly>, fetchOptions: FetchOptions = {}, ): Promise> { const completeFetchOptions = this.#getCompleteFetchOptions( @@ -489,7 +490,7 @@ export class RpcService implements AbstractRpcService { * @returns The complete set of `fetch` options. */ #getCompleteFetchOptions( - jsonRpcRequest: JsonRpcRequest, + jsonRpcRequest: Readonly>, fetchOptions: FetchOptions, ): FetchOptions { const defaultOptions = { diff --git a/yarn.lock b/yarn.lock index 9f85a346646..d9194baf9e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4238,11 +4238,13 @@ __metadata: "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^11.8.1" "@ts-bridge/cli": "npm:^0.6.4" + "@types/deep-freeze-strict": "npm:^1.1.0" "@types/jest": "npm:^27.4.1" "@types/jest-when": "npm:^2.7.3" "@types/lodash": "npm:^4.14.191" "@types/node-fetch": "npm:^2.6.12" async-mutex: "npm:^0.5.0" + deep-freeze-strict: "npm:^1.1.1" deepmerge: "npm:^4.2.2" fast-deep-equal: "npm:^3.1.3" immer: "npm:^9.0.6" From eb336944743e55715b69410746a060c310768ab3 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sat, 8 Nov 2025 09:29:08 -0800 Subject: [PATCH 25/30] docs: Document engine migration pitfalls --- packages/json-rpc-engine/README.md | 57 +++++++++++++++++++++++++- packages/json-rpc-engine/src/README.md | 5 +++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/json-rpc-engine/README.md b/packages/json-rpc-engine/README.md index ff10d2dc7f3..37edded5ed1 100644 --- a/packages/json-rpc-engine/README.md +++ b/packages/json-rpc-engine/README.md @@ -12,8 +12,10 @@ or ## Usage -> [!NOTE] +> [!TIP] > For the legacy `JsonRpcEngine`, see [its readme](./src/README.md). +> +> For how to migrate from the legacy `JsonRpcEngine` to `JsonRpcEngineV2`, see [Migrating from `JsonRpcEngine`](#migrating-from-jsonrpcengine). ```ts import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; @@ -784,6 +786,59 @@ to the response object via the `error` property. > appropriate request objects. `JsonRpcServer.handle()` will not type error at compile time if you attempt to pass > it an unsupported request object. +## Migrating from `JsonRpcEngine` + +Migrating from the legacy `JsonRpcEngine` to `JsonRpcEngineV2` is generally straightforward. +For an example, see [MetaMask/core#7065](https://github.com/MetaMask/core/pull/7065). +There are a couple of pitfalls to watch out for: + +### `MiddlewareContext` vs. non-JSON-RPC string properties + +The legacy `JsonRpcEngine` allowed non-JSON-RPC string properties to be attached to the request object. +`JsonRpcEngineV2` does not allow this, and instead you must use the `context` object to pass data between middleware. +While it's easy to migrate a middleware function body to use the `context` object, injected dependencies +of the middleware function may need to be updated. + +For example if you have a legacy middleware implementation like this: + +```ts +const createFooMiddleware = + (processFoo: (req: JsonRpcRequest) => string) => (req, res, next, end) => { + if (req.method === 'foo') { + const fooResult = processFoo(req); // May expect non-JSON-RPC properties on the request object! + res.result = fooResult; + end(); + } else { + next(); + } + }; +``` + +`processFoo` may expect non-JSON-RPC properties on the request object. To fully migrate the middleware, you need to +investigate the implementation of `processFoo` and potentially update it to accept a `context` object. + +### Frozen requests + +In the legacy `JsonRpcEngine`, request and response objects are mutable and shared between all middleware. +In `JsonRpcEngineV2`, response objects are not visible to middleware, and request objects are deeply frozen. +If injected dependencies mutate the request object, it will cause an error. + +For example, if you have a legacy middleware implementation like this: + +```ts +const createBarMiddleware = + (processBar: (req: JsonRpcRequest) => string) => (req, _res, next, _end) => { + if (req.method === 'bar') { + processBar(req); // May mutate the request object! + } + next(); + }; +``` + +`processBar` may mutate the request object. To fully migrate the middleware, you need to +investigate the implementation of `processBar` and update it to not directly mutate the request object. +See [Request modification](#request-modification) for how to modify the request object in `JsonRpcEngineV2`. + ## Contributing This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/json-rpc-engine/src/README.md b/packages/json-rpc-engine/src/README.md index 48be9407e05..b3d41fd979b 100644 --- a/packages/json-rpc-engine/src/README.md +++ b/packages/json-rpc-engine/src/README.md @@ -4,6 +4,11 @@ The deprecated, original `JsonRpcEngine` implementation. To be removed once the rest of MetaMask's codebase has been migrated to `JsonRpcEngineV2`. +> [!TIP] +> For the new `JsonRpcEngineV2`, see [the package readme](../README.md). +> +> For how to migrate from the legacy `JsonRpcEngine` to `JsonRpcEngineV2`, see [this package readme section](../README.md#migrating-from-jsonrpcengine). + ## Usage ```js From 5ec9464335c38ed957894457ea7a8355e42cd610 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:08:20 -0800 Subject: [PATCH 26/30] test: Cover more cases in fetch.test.ts --- .../eth-json-rpc-middleware/src/fetch.test.ts | 116 +++++++----------- packages/eth-json-rpc-middleware/src/fetch.ts | 3 - 2 files changed, 47 insertions(+), 72 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/fetch.test.ts b/packages/eth-json-rpc-middleware/src/fetch.test.ts index 4ff11297403..917abbd560d 100644 --- a/packages/eth-json-rpc-middleware/src/fetch.test.ts +++ b/packages/eth-json-rpc-middleware/src/fetch.test.ts @@ -10,81 +10,59 @@ import type { AbstractRpcServiceLike } from './types'; import { createRequest } from '../test/util/helpers'; describe('createFetchMiddleware', () => { - it('calls the RPC service with the correct request headers and body when no `originHttpHeaderKey` option given', async () => { - const rpcService = createRpcService(); - const requestSpy = jest.spyOn(rpcService, 'request'); - - const engine = JsonRpcEngineV2.create({ - middleware: [ - createFetchMiddleware({ - rpcService, - }), - ], - }); - await engine.handle( - createRequest({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }), - ); + it.each([ + [undefined, undefined], + [undefined, 'somedapp.com'], + ['X-Dapp-Origin', undefined], + ['X-Dapp-Origin', 'somedapp.com'], + ])( + 'calls the RPC service with the correct request headers and body with originHttpHeaderKey="%s" and origin="%s"', + async (originHttpHeaderKey, origin) => { + const rpcService = createRpcService(); + const requestSpy = jest.spyOn(rpcService, 'request'); - expect(requestSpy).toHaveBeenCalledWith( - { - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }, - { - headers: {}, - }, - ); - }); + const engine = JsonRpcEngineV2.create({ + middleware: [ + createFetchMiddleware({ + rpcService, + options: { + originHttpHeaderKey, + }, + }), + ], + }); - it('includes the `origin` from the given request in the request headers under the given `originHttpHeaderKey`', async () => { - const rpcService = createRpcService(); - const requestSpy = jest.spyOn(rpcService, 'request'); + const context = new MiddlewareContext<{ origin: string }>( + // eslint-disable-next-line jest/no-conditional-in-test + origin ? { origin } : [], + ); + const expectedHeaders = + // eslint-disable-next-line jest/no-conditional-in-test + originHttpHeaderKey && origin ? { [originHttpHeaderKey]: origin } : {}; - const engine = JsonRpcEngineV2.create({ - middleware: [ - createFetchMiddleware({ - rpcService, - options: { - originHttpHeaderKey: 'X-Dapp-Origin', - }, + await engine.handle( + createRequest({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], }), - ], - }); - const context = new MiddlewareContext<{ origin: string }>([ - ['origin', 'somedapp.com'], - ]); + { context }, + ); - await engine.handle( - createRequest({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }), - { context }, - ); - - expect(requestSpy).toHaveBeenCalledWith( - { - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }, - { - headers: { - 'X-Dapp-Origin': 'somedapp.com', + expect(requestSpy).toHaveBeenCalledWith( + { + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], }, - }, - ); - }); + { + headers: expectedHeaders, + }, + ); + }, + ); describe('if the response from the service does not contain an `error` field', () => { it('returns a successful JSON-RPC response containing the value of the `result` field', async () => { diff --git a/packages/eth-json-rpc-middleware/src/fetch.ts b/packages/eth-json-rpc-middleware/src/fetch.ts index 63812406aaf..96d1117b5c0 100644 --- a/packages/eth-json-rpc-middleware/src/fetch.ts +++ b/packages/eth-json-rpc-middleware/src/fetch.ts @@ -54,9 +54,6 @@ export function createFetchMiddleware({ data: jsonRpcResponse.error, }); } - - // Discard the `id` and `jsonrpc` fields in the response body - // (the JSON-RPC engine will fill those in) return jsonRpcResponse.result; }; } From 393388c4d114acbe51e4d836f8977f8ca738d184 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:11:35 -0800 Subject: [PATCH 27/30] refactor: Simplify condition in fetch.ts --- packages/eth-json-rpc-middleware/jest.config.js | 2 +- packages/eth-json-rpc-middleware/src/fetch.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/eth-json-rpc-middleware/jest.config.js b/packages/eth-json-rpc-middleware/jest.config.js index 5d557d1b1cb..111dcceb926 100644 --- a/packages/eth-json-rpc-middleware/jest.config.js +++ b/packages/eth-json-rpc-middleware/jest.config.js @@ -20,7 +20,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 81.85, + branches: 81.77, functions: 90.66, lines: 89.13, statements: 89.2, diff --git a/packages/eth-json-rpc-middleware/src/fetch.ts b/packages/eth-json-rpc-middleware/src/fetch.ts index 96d1117b5c0..bd5dc886a3d 100644 --- a/packages/eth-json-rpc-middleware/src/fetch.ts +++ b/packages/eth-json-rpc-middleware/src/fetch.ts @@ -35,9 +35,7 @@ export function createFetchMiddleware({ return async ({ request, context }) => { const origin = context.get('origin'); const headers = - 'originHttpHeaderKey' in options && - options.originHttpHeaderKey !== undefined && - origin !== undefined + options.originHttpHeaderKey !== undefined && origin !== undefined ? { [options.originHttpHeaderKey]: origin } : {}; From 0339e883b341a0f158f61a4b9dff132e45becaff Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:43:13 -0800 Subject: [PATCH 28/30] refactor: Asynchronously handle both results and errors in inflight-cache --- .../eth-json-rpc-middleware/jest.config.js | 2 +- .../src/inflight-cache.ts | 93 ++++++++----------- 2 files changed, 41 insertions(+), 54 deletions(-) diff --git a/packages/eth-json-rpc-middleware/jest.config.js b/packages/eth-json-rpc-middleware/jest.config.js index 111dcceb926..4c500d749cb 100644 --- a/packages/eth-json-rpc-middleware/jest.config.js +++ b/packages/eth-json-rpc-middleware/jest.config.js @@ -20,7 +20,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 81.77, + branches: 81.49, functions: 90.66, lines: 89.13, statements: 89.2, diff --git a/packages/eth-json-rpc-middleware/src/inflight-cache.ts b/packages/eth-json-rpc-middleware/src/inflight-cache.ts index e6062678c8e..c488cc80bbf 100644 --- a/packages/eth-json-rpc-middleware/src/inflight-cache.ts +++ b/packages/eth-json-rpc-middleware/src/inflight-cache.ts @@ -11,10 +11,8 @@ import { import { projectLogger, createModuleLogger } from './logging-utils'; import { cacheIdentifierForRequest } from './utils/cache'; -type RequestHandler = { - onSuccess: (result: Json) => void; - onError: (error: Error) => void; -}; +type RequestHandler = [(result: Json) => void, (error: Error) => void]; + type InflightRequest = { [cacheId: string]: RequestHandler[]; }; @@ -72,7 +70,7 @@ export function createInflightCacheMiddleware(): JsonRpcMiddleware< activeRequestHandlers.length, request, ); - handleSuccess(result, activeRequestHandlers); + runRequestHandlers({ result }, activeRequestHandlers); return result; } catch (error) { log( @@ -80,60 +78,49 @@ export function createInflightCacheMiddleware(): JsonRpcMiddleware< activeRequestHandlers.length, request, ); - handleError(error as Error, activeRequestHandlers); + runRequestHandlers({ error: error as Error }, activeRequestHandlers); throw error; } finally { delete inflightRequests[cacheId]; } }; +} - /** - * Creates a new request handler for the active request. - * - * @param activeRequestHandlers - The active request handlers. - * @returns A promise that resolves to the result of the request. - */ - function createActiveRequestHandler( - activeRequestHandlers: RequestHandler[], - ): Promise { - const { resolve, promise, reject } = createDeferredPromise(); - activeRequestHandlers.push({ - onSuccess: (result: Json) => resolve(result), - onError: (error: Error) => reject(error), - }); - return promise; - } - - /** - * Handles successful requests. - * - * @param result - The result of the request. - * @param activeRequestHandlers - The active request handlers. - */ - function handleSuccess( - result: Json, - activeRequestHandlers: RequestHandler[], - ): void { - // use setTimeout so we can resolve our original request first - setTimeout(() => { - activeRequestHandlers.forEach(({ onSuccess }) => { - onSuccess(result); - }); - }); - } +/** + * Creates a new request handler for the active request. + * + * @param activeRequestHandlers - The active request handlers. + * @returns A promise that resolves to the result of the request. + */ +function createActiveRequestHandler( + activeRequestHandlers: RequestHandler[], +): Promise { + const { resolve, promise, reject } = createDeferredPromise(); + activeRequestHandlers.push([ + (result: Json) => resolve(result), + (error: Error) => reject(error), + ]); + return promise; +} - /** - * Handles failed requests. - * - * @param error - The error of the request. - * @param activeRequestHandlers - The active request handlers. - */ - function handleError( - error: Error, - activeRequestHandlers: RequestHandler[], - ): void { - activeRequestHandlers.forEach(({ onError }) => { - onError(error); +/** + * Runs the request handlers for the given result or error. + * + * @param resultOrError - The result or error of the request. + * @param activeRequestHandlers - The active request handlers. + */ +function runRequestHandlers( + resultOrError: { result: Json } | { error: Error }, + activeRequestHandlers: RequestHandler[], +): void { + // use setTimeout so we can handle the original request first + setTimeout(() => { + activeRequestHandlers.forEach(([onSuccess, onError]) => { + if ('result' in resultOrError) { + onSuccess(resultOrError.result); + } else { + onError(resultOrError.error); + } }); - } + }); } From 44a680d72350da55b676424436288dc88d366a6c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:02:48 -0800 Subject: [PATCH 29/30] test: Add providerAsMiddleware error test cases --- .../src/providerAsMiddleware.test.ts | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts index ce1376a304e..3257ee3a824 100644 --- a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts @@ -2,7 +2,10 @@ import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import type { Json } from '@metamask/utils'; -import { assertIsJsonRpcSuccess } from '@metamask/utils'; +import { + assertIsJsonRpcFailure, + assertIsJsonRpcSuccess, +} from '@metamask/utils'; import { providerAsMiddleware, @@ -10,9 +13,12 @@ import { } from './providerAsMiddleware'; import { createRequest } from '../test/util/helpers'; -const createMockProvider = (result: Json): InternalProvider => +const createMockProvider = (resultOrError: Json | Error): InternalProvider => ({ - request: jest.fn().mockResolvedValue(result), + request: + resultOrError instanceof Error + ? jest.fn().mockRejectedValue(resultOrError) + : jest.fn().mockResolvedValue(resultOrError), }) as unknown as InternalProvider; describe('providerAsMiddleware', () => { @@ -31,10 +37,45 @@ describe('providerAsMiddleware', () => { await new Promise((resolve) => { engine.handle(request, (error, response) => { expect(error).toBeNull(); - expect(response).toBeDefined(); assertIsJsonRpcSuccess(response); expect(response.result).toStrictEqual(mockResult); expect(mockProvider.request).toHaveBeenCalledWith(request); + + resolve(); + }); + }); + }); + + it('forwards errors to the provider and returns the error', async () => { + const mockError = new Error('test'); + const mockProvider = createMockProvider(mockError); + + const engine = new JsonRpcEngine(); + engine.push(providerAsMiddleware(mockProvider)); + + const request = createRequest({ + method: 'eth_chainId', + params: [], + }); + + await new Promise((resolve) => { + engine.handle(request, (error, response) => { + assertIsJsonRpcFailure(response); + expect(error).toBe(mockError); + expect(response.error).toStrictEqual( + expect.objectContaining({ + message: mockError.message, + code: -32603, + data: { + cause: { + message: mockError.message, + stack: expect.any(String), + }, + }, + }), + ); + expect(mockProvider.request).toHaveBeenCalledWith(request); + resolve(); }); }); @@ -60,4 +101,21 @@ describe('providerAsMiddlewareV2', () => { expect(result).toStrictEqual(mockResult); expect(mockProvider.request).toHaveBeenCalledWith(request); }); + + it('forwards errors to the provider and returns the error', async () => { + const mockError = new Error('test'); + const mockProvider = createMockProvider(mockError); + + const engine = JsonRpcEngineV2.create({ + middleware: [providerAsMiddlewareV2(mockProvider)], + }); + + const request = createRequest({ + method: 'eth_chainId', + params: [], + }); + + await expect(engine.handle(request)).rejects.toThrow(mockError); + expect(mockProvider.request).toHaveBeenCalledWith(request); + }); }); From ae42d6d0fcfa21146971e38bd2cfee59a61c0a9f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:13:38 -0800 Subject: [PATCH 30/30] docs: Tweak eth-json-rpc-middleware changelog --- packages/eth-json-rpc-middleware/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index 5dc3ad3053f..893803340bb 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Migrate to `JsonRpcEngineV2` ([#7065](https://github.com/MetaMask/core/pull/7065)) - Migrates all middleware from `JsonRpcEngine` to `JsonRpcEngineV2`. + - Signatures of various middleware dependencies, e.g. `processTransaction` of `createWalletMiddleware`, have changed + and must be updated by consumers. + - Be advised that request objects are now deeply frozen, and cannot be mutated. - To continue using this package with the legacy `JsonRpcEngine`, use the `asLegacyMiddleware` backwards compatibility function. - **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.