diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 9697b47c0f4..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 }, @@ -165,11 +164,10 @@ "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, - "jsdoc/require-jsdoc": 1 + "jsdoc/match-description": 2 }, "packages/eth-json-rpc-middleware/src/fetch.test.ts": { "jsdoc/match-description": 1 @@ -178,8 +176,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 @@ -188,7 +185,7 @@ "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 @@ -215,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 @@ -226,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/jest.config.packages.js b/jest.config.packages.js index ce6f1f267d3..b3406afaf17 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -80,6 +80,9 @@ module.exports = { // Here we ensure that Jest resolves `@metamask/*` imports to the uncompiled source code for packages that live in this repo. // NOTE: This must be synchronized with the `paths` option in `tsconfig.packages.json`. moduleNameMapper: { + '^@metamask/json-rpc-engine/v2$': [ + '/../json-rpc-engine/src/v2/index.ts', + ], '^@metamask/(.+)$': [ '/../$1/src', // Some @metamask/* packages we are referencing aren't in this monorepo, diff --git a/packages/eth-block-tracker/tests/withBlockTracker.ts b/packages/eth-block-tracker/tests/withBlockTracker.ts index 39c227da909..ca866459f57 100644 --- a/packages/eth-block-tracker/tests/withBlockTracker.ts +++ b/packages/eth-block-tracker/tests/withBlockTracker.ts @@ -1,8 +1,4 @@ -import { providerFromEngine } from '@metamask/eth-json-rpc-provider'; -import type { - // Eip1193Request, - InternalProvider, -} from '@metamask/eth-json-rpc-provider'; +import { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { Json } from '@metamask/utils'; import util from 'util'; @@ -98,7 +94,7 @@ function getFakeProvider({ }); } - const provider = providerFromEngine(new JsonRpcEngine()); + const provider = new InternalProvider({ engine: new JsonRpcEngine() }); jest .spyOn(provider, 'request') .mockImplementation(async (eip1193Request): Promise => { diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index d8f6c686f1e..389fc695588 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -9,8 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Migrate to `JsonRpcEngineV2` ([#6976](https://github.com/MetaMask/core/pull/6976)) + - 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)) + - The `retryOnEmpty` middleware will now throw an error if it encounters an `undefined` result when dispatching + a request with a later block number than the originally requested block number. + - In practice, this should happen rarely if ever. - Migrate all uses of `interface` to `type` ([#6885](https://github.com/MetaMask/core/pull/6885)) ## [21.0.0] diff --git a/packages/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..0c92d8c2a7b 100644 --- a/packages/eth-json-rpc-middleware/src/block-cache.ts +++ b/packages/eth-json-rpc-middleware/src/block-cache.ts @@ -1,6 +1,9 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +import type { + JsonRpcMiddleware, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; import { projectLogger, createModuleLogger } from './logging-utils'; import type { @@ -8,8 +11,6 @@ import type { BlockCache, // eslint-disable-next-line @typescript-eslint/no-shadow Cache, - JsonRpcCacheMiddleware, - JsonRpcRequestToCache, } from './types'; import { cacheIdentifierForRequest, @@ -21,7 +22,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 +99,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 +135,17 @@ class BlockCacheStrategy { export function createBlockCacheMiddleware({ blockTracker, -}: BlockCacheMiddlewareOptions = {}): JsonRpcCacheMiddleware< - JsonRpcParams, - Json +}: BlockCacheMiddlewareOptions = {}): JsonRpcMiddleware< + JsonRpcRequest, + 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 +154,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/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/block-ref.test.ts b/packages/eth-json-rpc-middleware/src/block-ref.test.ts index 6ea0d7db0ea..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,3 +1,5 @@ +import { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; + import { createBlockRefMiddleware } from '.'; import { createMockParamsWithBlockParamAt, @@ -9,6 +11,7 @@ import { expectProviderRequestNotToHaveBeenMade, createProviderAndBlockTracker, createEngine, + createRequest, } from '../test/util/helpers'; describe('createBlockRefMiddleware', () => { @@ -55,15 +58,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 +80,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 +96,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 +133,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 +152,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 +168,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 +207,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 +239,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 +278,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 +307,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/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; } 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; + }; } 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/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 aa2620135fa..e6062678c8e 100644 --- a/packages/eth-json-rpc-middleware/src/inflight-cache.ts +++ b/packages/eth-json-rpc-middleware/src/inflight-cache.ts @@ -1,112 +1,139 @@ -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; import type { - JsonRpcParams, - Json, - PendingJsonRpcResponse, + 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, JsonRpcCacheMiddleware } 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[]; }; 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/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 new file mode 100644 index 00000000000..ce1376a304e --- /dev/null +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts @@ -0,0 +1,63 @@ +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 { + providerAsMiddleware, + providerAsMiddlewareV2, +} from './providerAsMiddleware'; +import { createRequest } from '../test/util/helpers'; + +const createMockProvider = (result: Json): InternalProvider => + ({ + request: jest.fn().mockResolvedValue(result), + }) as unknown as InternalProvider; + +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).toStrictEqual(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).toStrictEqual(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); +} 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 2b454ff4fef..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'; @@ -21,11 +20,7 @@ import { timeout } from './utils/timeout'; const log = createModuleLogger(projectLogger, 'retry-on-empty'); // empty values used to determine if a request should be retried // `` comes from https://github.com/ethereum/go-ethereum/issues/16925 -const emptyValues: (string | null | undefined)[] = [ - undefined, - null, - '\u003cnil\u003e', -]; +const emptyValues = [null, '\u003cnil\u003e']; /** * Creates a middleware that retries requests with empty responses. @@ -41,7 +36,7 @@ export function createRetryOnEmptyMiddleware({ }: { provider?: InternalProvider; blockTracker?: PollingBlockTracker; -} = {}): JsonRpcMiddleware { +} = {}): JsonRpcMiddleware { if (!provider) { throw Error( 'RetryOnEmptyMiddleware - mandatory "provider" option is missing.', @@ -54,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) { @@ -102,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); @@ -122,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/src/types.ts b/packages/eth-json-rpc-middleware/src/types.ts index 14070671dd7..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, @@ -6,22 +5,6 @@ import type { JsonRpcResponse, } from '@metamask/utils'; -export type JsonRpcRequestToCache = - JsonRpcRequest & { - 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/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); } // diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index dd4b6ce2d2a..a1a691b779d 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -1,12 +1,8 @@ import { PollingBlockTracker } from '@metamask/eth-block-tracker'; -import { - providerFromEngine, - type InternalProvider, -} from '@metamask/eth-json-rpc-provider'; -import { - JsonRpcEngine, - type JsonRpcMiddleware, -} from '@metamask/json-rpc-engine'; +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 { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona/full'; import { isDeepStrictEqual } from 'util'; @@ -69,22 +65,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; - } - - res.jsonrpc ??= '2.0'; - - if (res.result === undefined) { - res.result = 'default result'; +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'; } - - end(); + return result; }); } @@ -94,9 +83,12 @@ 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 = providerFromEngine(engine); + const provider = new InternalProvider({ engine }); const blockTracker = new PollingBlockTracker({ provider, @@ -117,27 +109,14 @@ export function createProviderAndBlockTracker() { export function createEngine( middlewareUnderTest: JsonRpcMiddleware, ...otherMiddleware: JsonRpcMiddleware[] -): JsonRpcEngine { - const engine = new JsonRpcEngine(); - engine.push(middlewareUnderTest); - if (otherMiddleware.length === 0) { - otherMiddleware.push(createFinalMiddlewareWithDefaultResult()); - } - for (const middleware of otherMiddleware) { - engine.push(middleware); - } - return engine; -} - -/** - * 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(); +): JsonRpcEngineV2 { + return JsonRpcEngineV2.create({ + middleware: [ + middlewareUnderTest, + ...(otherMiddleware.length === 0 + ? [createFinalMiddlewareWithDefaultResult()] + : otherMiddleware), + ], }); } 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"] } diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index f108a4df337..b564b68d55e 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -7,11 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `providerFromMiddlewareV2` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - This accepts the new middleware from `@metamask/json-rpc-engine/v2`. + ### Changed - **BREAKING:** Replace `SafeEventEmitterProvider` with `InternalProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - The new class is behaviorally equivalent to the previous version except it does not extend `SafeEventEmitter`. - `SafeEventEmitterProvider` is for now still exported as a deprecated alias of `InternalProvider` for backwards compatibility. +- **BREAKING:** Migrate from `JsonRpcEngine` to `JsonRpcEngineV2` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcEngineV2` internally wherever they appear. + This change should mostly be unobservable. However, due to differences in error handling, this may be breaking for consumers. + +### Deprecated + +- Deprecate `providerFromMiddleware` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - Use `providerFromMiddlewareV2` instead, which supports the new middleware from `@metamask/json-rpc-engine/v2`. + +### Removed + +- **BREAKING:** Remove `providerFromEngine` ([#7001](https://github.com/MetaMask/core/pull/7001)) + - Use `InternalProvider` directly instead. ## [5.0.1] diff --git a/packages/eth-json-rpc-provider/package.json b/packages/eth-json-rpc-provider/package.json index 1796e2e4436..c897f59db8f 100644 --- a/packages/eth-json-rpc-provider/package.json +++ b/packages/eth-json-rpc-provider/package.json @@ -56,7 +56,7 @@ "@metamask/json-rpc-engine": "^10.1.1", "@metamask/rpc-errors": "^7.0.2", "@metamask/utils": "^11.8.1", - "uuid": "^8.3.2" + "nanoid": "^3.3.8" }, "devDependencies": { "@ethersproject/providers": "^5.7.0", diff --git a/packages/eth-json-rpc-provider/src/index.test.ts b/packages/eth-json-rpc-provider/src/index.test.ts index 1c94f265e90..1fa713b79bd 100644 --- a/packages/eth-json-rpc-provider/src/index.test.ts +++ b/packages/eth-json-rpc-provider/src/index.test.ts @@ -2,12 +2,12 @@ import * as allExports from '.'; describe('Package exports', () => { it('has expected exports', () => { - expect(Object.keys(allExports)).toMatchInlineSnapshot(` + expect(Object.keys(allExports).sort()).toMatchInlineSnapshot(` Array [ "InternalProvider", "SafeEventEmitterProvider", - "providerFromEngine", "providerFromMiddleware", + "providerFromMiddlewareV2", ] `); }); diff --git a/packages/eth-json-rpc-provider/src/index.ts b/packages/eth-json-rpc-provider/src/index.ts index a16f98b675c..a4bab7a5db3 100644 --- a/packages/eth-json-rpc-provider/src/index.ts +++ b/packages/eth-json-rpc-provider/src/index.ts @@ -1,6 +1,5 @@ import { InternalProvider } from './internal-provider'; -export * from './provider-from-engine'; export * from './provider-from-middleware'; /** diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index 3e6a897746a..6e102647d9e 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -1,13 +1,13 @@ import { Web3Provider } from '@ethersproject/providers'; import EthQuery from '@metamask/eth-query'; import EthJsQuery from '@metamask/ethjs-query'; -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import { providerErrors } from '@metamask/rpc-errors'; +import { asV2Middleware, JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; +import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { type JsonRpcRequest, type Json } from '@metamask/utils'; import { BrowserProvider } from 'ethers'; import { promisify } from 'util'; -// eslint-disable-next-line import-x/namespace -import * as uuid from 'uuid'; import { InternalProvider, @@ -16,29 +16,55 @@ import { jest.mock('uuid'); -/** - * Creates a mock JSON-RPC engine that returns a predefined response for a specific method. - * - * @param method - The RPC method to mock. - * @param response - The response to return for the mocked method. - * @returns A JSON-RPC engine instance with the mocked method. - */ -function createMockEngine(method: string, response: Json) { +type ResultParam = Json | ((req?: JsonRpcRequest) => Json); + +const createLegacyEngine = (method: string, result: ResultParam) => { const engine = new JsonRpcEngine(); engine.push((req, res, next, end) => { if (req.method === method) { - res.result = response; + res.result = typeof result === 'function' ? result(req) : result; return end(); } return next(); }); return engine; -} +}; + +const createV2Engine = (method: string, result: ResultParam) => { + return JsonRpcEngineV2.create>({ + middleware: [ + ({ request, next }) => { + if (request.method === method) { + return typeof result === 'function' ? result(request) : result; + } + return next(); + }, + ], + }); +}; + +describe('legacy constructor', () => { + it('can be constructed with an engine', () => { + const provider = new InternalProvider({ + engine: createLegacyEngine('eth_blockNumber', 42), + }); + expect(provider).toBeDefined(); + }); +}); -describe('InternalProvider', () => { +describe.each([ + { + createRpcHandler: createLegacyEngine, + name: 'JsonRpcEngine', + }, + { + createRpcHandler: createV2Engine, + name: 'JsonRpcServer', + }, +] as const)('InternalProvider with $name', ({ createRpcHandler }) => { it('returns the correct block number with @metamask/eth-query', async () => { const provider = new InternalProvider({ - engine: createMockEngine('eth_blockNumber', 42), + engine: createRpcHandler('eth_blockNumber', 42), }); const ethQuery = new EthQuery(provider); @@ -49,7 +75,7 @@ describe('InternalProvider', () => { it('returns the correct block number with @metamask/ethjs-query', async () => { const provider = new InternalProvider({ - engine: createMockEngine('eth_blockNumber', 42), + engine: createRpcHandler('eth_blockNumber', 42), }); const ethJsQuery = new EthJsQuery(provider); @@ -60,7 +86,7 @@ describe('InternalProvider', () => { it('returns the correct block number with Web3Provider', async () => { const provider = new InternalProvider({ - engine: createMockEngine('eth_blockNumber', 42), + engine: createRpcHandler('eth_blockNumber', 42), }); const web3Provider = new Web3Provider(provider); @@ -71,27 +97,27 @@ describe('InternalProvider', () => { it('returns the correct block number with BrowserProvider', async () => { const provider = new InternalProvider({ - engine: createMockEngine('eth_blockNumber', 42), + engine: createRpcHandler('eth_blockNumber', 42), }); const browserProvider = new BrowserProvider(provider); const response = await browserProvider.send('eth_blockNumber', []); expect(response).toBe(42); + + browserProvider.destroy(); }); describe('request', () => { it('handles a successful JSON-RPC object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); - const exampleRequest = { - id: 1, + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + id: '1', jsonrpc: '2.0' as const, method: 'test', params: { @@ -100,10 +126,10 @@ describe('InternalProvider', () => { }, }; - const result = await provider.request(exampleRequest); + const result = await provider.request(request); expect(req).toStrictEqual({ - id: 1, + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -115,27 +141,24 @@ describe('InternalProvider', () => { }); it('handles a successful EIP-1193 object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); - const exampleRequest = { + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { method: 'test', params: { param1: 'value1', param2: 'value2', }, }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - const result = await provider.request(exampleRequest); + const result = await provider.request(request); expect(req).toStrictEqual({ - id: 'mock-id', + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -147,61 +170,43 @@ describe('InternalProvider', () => { }); it('handles a failure with a non-JSON-RPC error', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, _res, _next, end) => { - end( - providerErrors.custom({ - code: 1001, - message: 'Test error', - data: { - cause: 'Test cause', - }, - }), - ); + const rpcHandler = createRpcHandler('test', () => { + throw providerErrors.custom({ + code: 1001, + message: 'Test error', + data: { cause: 'Test cause' }, + }); }); - const provider = new InternalProvider({ engine }); - const exampleRequest = { - id: 1, + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + id: '1', jsonrpc: '2.0' as const, method: 'test', }; - await expect(async () => - provider.request(exampleRequest), - ).rejects.toThrow( - expect.objectContaining({ + await expect(async () => provider.request(request)).rejects.toThrow( + providerErrors.custom({ code: 1001, message: 'Test error', data: { cause: 'Test cause' }, - stack: expect.stringContaining('internal-provider.test.ts:'), }), ); }); it('handles a failure with a JSON-RPC error', async () => { - const engine = new JsonRpcEngine(); - engine.push(() => { + const rpcHandler = createRpcHandler('test', () => { throw new Error('Test error'); }); - const provider = new InternalProvider({ engine }); - const exampleRequest = { - id: 1, + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + id: '1', jsonrpc: '2.0' as const, method: 'test', }; - await expect(async () => - provider.request(exampleRequest), - ).rejects.toThrow( - expect.objectContaining({ - code: -32603, + await expect(async () => provider.request(request)).rejects.toThrow( + rpcErrors.internal({ message: 'Test error', - data: { - cause: expect.objectContaining({ - stack: expect.stringContaining('internal-provider.test.ts:'), - message: 'Test error', - }), - }, }), ); }); @@ -209,17 +214,15 @@ describe('InternalProvider', () => { describe('sendAsync', () => { it('handles a successful JSON-RPC object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); - const exampleRequest = { - id: 1, + const request = { + id: '1', jsonrpc: '2.0' as const, method: 'test', params: { @@ -228,10 +231,10 @@ describe('InternalProvider', () => { }, }; - const response = await promisifiedSendAsync(exampleRequest); + const response = await promisifiedSendAsync(request); expect(req).toStrictEqual({ - id: 1, + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -243,28 +246,25 @@ describe('InternalProvider', () => { }); it('handles a successful EIP-1193 object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); - const exampleRequest = { + const request = { method: 'test', params: { param1: 'value1', param2: 'value2', }, }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - const response = await promisifiedSendAsync(exampleRequest); + const response = await promisifiedSendAsync(request); expect(req).toStrictEqual({ - id: 'mock-id', + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -276,51 +276,74 @@ describe('InternalProvider', () => { }); it('handles a failed request', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, _res, _next, _end) => { + const rpcHandler = createRpcHandler('test', () => { throw new Error('Test error'); }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSendAsync = promisify(provider.sendAsync); - const exampleRequest = { - id: 1, + const request = { + id: '1', jsonrpc: '2.0' as const, method: 'test', }; - await expect(async () => - promisifiedSendAsync(exampleRequest), - ).rejects.toThrow('Test error'); + await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( + 'Test error', + ); + }); + + it('handles an error thrown by the JSON-RPC handler', async () => { + let rpcHandler = createRpcHandler('test', () => null); + // Transform the engine into a server so we can mock the "handle" method. + // The "handle" method should never throw, but we should be resilient to it anyway. + rpcHandler = + // eslint-disable-next-line jest/no-conditional-in-test + 'push' in rpcHandler + ? JsonRpcEngineV2.create({ middleware: [asV2Middleware(rpcHandler)] }) + : rpcHandler; + jest + .spyOn(rpcHandler, 'handle') + .mockRejectedValue(new Error('Test error')); + const provider = new InternalProvider({ engine: rpcHandler }); + const promisifiedSendAsync = promisify(provider.sendAsync); + const request = { + id: '1', + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => promisifiedSendAsync(request)).rejects.toThrow( + 'Test error', + ); }); }); describe('send', () => { it('throws if a callback is not provided', () => { - const engine = new JsonRpcEngine(); - const provider = new InternalProvider({ engine }); - const exampleRequest = { - id: 1, + const rpcHandler = createRpcHandler('test', 42); + const provider = new InternalProvider({ engine: rpcHandler }); + const request = { + id: '1', jsonrpc: '2.0' as const, method: 'test', }; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(() => (provider.send as any)(exampleRequest)).toThrow(''); + // @ts-expect-error - Destructive testing. + expect(() => provider.send(request)).toThrow( + 'Must provide callback to "send" method.', + ); }); it('handles a successful JSON-RPC object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSend = promisify(provider.send); - const exampleRequest = { - id: 1, + const request = { + id: '1', jsonrpc: '2.0' as const, method: 'test', params: { @@ -329,10 +352,10 @@ describe('InternalProvider', () => { }, }; - const response = await promisifiedSend(exampleRequest); + const response = await promisifiedSend(request); expect(req).toStrictEqual({ - id: 1, + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -344,28 +367,25 @@ describe('InternalProvider', () => { }); it('handles a successful EIP-1193 object request', async () => { - const engine = new JsonRpcEngine(); let req: JsonRpcRequest | undefined; - engine.push((_req, res, _next, end) => { - req = _req; - res.result = 42; - end(); + const rpcHandler = createRpcHandler('test', (request) => { + req = request; + return 42; }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSend = promisify(provider.send); - const exampleRequest = { + const request = { method: 'test', params: { param1: 'value1', param2: 'value2', }, }; - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); - const response = await promisifiedSend(exampleRequest); + const response = await promisifiedSend(request); expect(req).toStrictEqual({ - id: 'mock-id', + id: expect.any(String), jsonrpc: '2.0' as const, method: 'test', params: { @@ -377,19 +397,18 @@ describe('InternalProvider', () => { }); it('handles a failed request', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, _res, _next, _end) => { + const rpcHandler = createRpcHandler('test', () => { throw new Error('Test error'); }); - const provider = new InternalProvider({ engine }); + const provider = new InternalProvider({ engine: rpcHandler }); const promisifiedSend = promisify(provider.send); - const exampleRequest = { - id: 1, + const request = { + id: '1', jsonrpc: '2.0' as const, method: 'test', }; - await expect(async () => promisifiedSend(exampleRequest)).rejects.toThrow( + await expect(async () => promisifiedSend(request)).rejects.toThrow( 'Test error', ); }); @@ -398,7 +417,6 @@ describe('InternalProvider', () => { describe('convertEip1193RequestToJsonRpcRequest', () => { it('generates a unique id if id is not provided', () => { - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -408,24 +426,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: 'mock-id', - jsonrpc: '2.0', - method: 'test', - params: { param1: 'value1', param2: 'value2' }, - }); - }); - - it('uses the provided id if id is provided', () => { - const eip1193Request = { - id: '123', - method: 'test', - params: { param1: 'value1', param2: 'value2' }, - }; - const jsonRpcRequest = - convertEip1193RequestToJsonRpcRequest(eip1193Request); - - expect(jsonRpcRequest).toStrictEqual({ - id: '123', + id: expect.any(String), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -433,7 +434,6 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { }); it('uses the default jsonrpc version if not provided', () => { - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -443,7 +443,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: 'mock-id', + id: expect.any(String), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -451,7 +451,6 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { }); it('uses the provided jsonrpc version if provided', () => { - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { jsonrpc: '2.0' as const, method: 'test', @@ -462,7 +461,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: 'mock-id', + id: expect.any(String), jsonrpc: '2.0', method: 'test', params: { param1: 'value1', param2: 'value2' }, @@ -470,7 +469,6 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { }); it('uses an empty object as params if not provided', () => { - jest.spyOn(uuid, 'v4').mockReturnValueOnce('mock-id'); const eip1193Request = { method: 'test', }; @@ -479,7 +477,7 @@ describe('convertEip1193RequestToJsonRpcRequest', () => { convertEip1193RequestToJsonRpcRequest(eip1193Request); expect(jsonRpcRequest).toStrictEqual({ - id: 'mock-id', + id: expect.any(String), jsonrpc: '2.0', method: 'test', }); diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index 9c1a59c0df2..920416c7317 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -1,49 +1,35 @@ -import type { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import { JsonRpcError } from '@metamask/rpc-errors'; +import { asV2Middleware, type JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { - Json, - JsonRpcId, - JsonRpcParams, - JsonRpcRequest, - JsonRpcVersion2, + ContextConstraint, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; +import type { JsonRpcSuccess } from '@metamask/utils'; +import { + type Json, + type JsonRpcId, + type JsonRpcParams, + type JsonRpcRequest, + type JsonRpcVersion2, } from '@metamask/utils'; -import { v4 as uuidV4 } from 'uuid'; +import { nanoid } from 'nanoid'; /** * A JSON-RPC request conforming to the EIP-1193 specification. */ -type Eip1193Request = { +type Eip1193Request = { id?: JsonRpcId; jsonrpc?: JsonRpcVersion2; method: string; params?: Params; }; -/** - * Converts an EIP-1193 request to a JSON-RPC request. - * - * @param eip1193Request - The EIP-1193 request to convert. - * @returns The corresponding JSON-RPC request. - */ -export function convertEip1193RequestToJsonRpcRequest< - Params extends JsonRpcParams, ->( - eip1193Request: Eip1193Request, -): JsonRpcRequest> { - const { id = uuidV4(), jsonrpc = '2.0', method, params } = eip1193Request; - return params - ? { - id, - jsonrpc, - method, - params, - } - : { - id, - jsonrpc, - method, - }; -} +type Options< + Request extends JsonRpcRequest = JsonRpcRequest, + Context extends ContextConstraint = MiddlewareContext, +> = { + engine: JsonRpcEngine | JsonRpcEngineV2; +}; /** * An Ethereum provider. @@ -52,16 +38,21 @@ export function convertEip1193RequestToJsonRpcRequest< * It is not compliant with any Ethereum provider standard. */ export class InternalProvider { - readonly #engine: JsonRpcEngine; + readonly #engine: JsonRpcEngineV2; /** - * Construct a InternalProvider from a JSON-RPC engine. + * Construct a InternalProvider from a JSON-RPC server or legacy engine. * * @param options - Options. * @param options.engine - The JSON-RPC engine used to process requests. */ - constructor({ engine }: { engine: JsonRpcEngine }) { - this.#engine = engine; + constructor({ engine }: Options) { + this.#engine = + 'push' in engine + ? JsonRpcEngineV2.create({ + middleware: [asV2Middleware(engine)], + }) + : engine; } /** @@ -75,24 +66,7 @@ export class InternalProvider { ): Promise { const jsonRpcRequest = convertEip1193RequestToJsonRpcRequest(eip1193Request); - const response = await this.#engine.handle< - Params | Record, - Result - >(jsonRpcRequest); - - if ('result' in response) { - return response.result; - } - - const error = new JsonRpcError( - response.error.code, - response.error.message, - response.error.data, - ); - if ('stack' in response.error) { - error.stack = response.error.stack; - } - throw error; + return (await this.#handle(jsonRpcRequest)).result; } /** @@ -103,17 +77,18 @@ export class InternalProvider { * * @param eip1193Request - The request to send. * @param callback - A function that is called upon the success or failure of the request. - * @deprecated Please use `request` instead. + * @deprecated Use {@link request} instead. This method is retained solely for backwards + * compatibility with certain libraries. */ sendAsync = ( eip1193Request: Eip1193Request, - // TODO: Replace `any` with type + // Non-polluting `any` that acts like a constraint. // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (error: unknown, providerRes?: any) => void, ) => { const jsonRpcRequest = convertEip1193RequestToJsonRpcRequest(eip1193Request); - this.#engine.handle(jsonRpcRequest, callback); + this.#handleWithCallback(jsonRpcRequest, callback); }; /** @@ -124,7 +99,8 @@ export class InternalProvider { * * @param eip1193Request - The request to send. * @param callback - A function that is called upon the success or failure of the request. - * @deprecated Please use `request` instead. + * @deprecated Use {@link request} instead. This method is retained solely for backwards + * compatibility with certain libraries. */ send = ( eip1193Request: Eip1193Request, @@ -137,6 +113,62 @@ export class InternalProvider { } const jsonRpcRequest = convertEip1193RequestToJsonRpcRequest(eip1193Request); - this.#engine.handle(jsonRpcRequest, callback); + this.#handleWithCallback(jsonRpcRequest, callback); }; + + readonly #handle = async ( + jsonRpcRequest: JsonRpcRequest, + ): Promise> => { + const { id, jsonrpc } = jsonRpcRequest; + // This typecast is technicaly unsafe, but we need it to preserve the provider's + // public interface, which allows you to typecast results. + const result = (await this.#engine.handle( + jsonRpcRequest, + )) as unknown as Result; + + return { + id, + jsonrpc, + result, + }; + }; + + readonly #handleWithCallback = ( + jsonRpcRequest: JsonRpcRequest, + callback: (error: unknown, providerRes?: unknown) => void, + ): void => { + /* eslint-disable promise/no-callback-in-promise */ + this.#handle(jsonRpcRequest) + // A resolution will always be a successful response + .then((response) => callback(null, response)) + .catch((error) => { + callback(error); + }); + /* eslint-enable promise/no-callback-in-promise */ + }; +} + +/** + * Convert an EIP-1193 request to a JSON-RPC request. + * + * @param eip1193Request - The EIP-1193 request to convert. + * @returns The JSON-RPC request. + */ +export function convertEip1193RequestToJsonRpcRequest( + eip1193Request: Eip1193Request, +): JsonRpcRequest { + const { id = nanoid(), jsonrpc = '2.0', method, params } = eip1193Request; + + return params + ? { + id, + jsonrpc, + method, + params, + } + : { + id, + jsonrpc, + method, + }; } diff --git a/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts b/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts deleted file mode 100644 index ca90a1deac1..00000000000 --- a/packages/eth-json-rpc-provider/src/provider-from-engine.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import { providerErrors } from '@metamask/rpc-errors'; - -import { providerFromEngine } from './provider-from-engine'; - -describe('providerFromEngine', () => { - it('handle a successful request', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, res, _next, end) => { - res.result = 42; - end(); - }); - const provider = providerFromEngine(engine); - const exampleRequest = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - const response = await provider.request(exampleRequest); - - expect(response).toBe(42); - }); - - it('handle a failed request', async () => { - const engine = new JsonRpcEngine(); - engine.push((_req, _res, _next, end) => { - end( - providerErrors.custom({ - code: 1001, - message: 'Test error', - }), - ); - }); - const provider = providerFromEngine(engine); - const exampleRequest = { - id: 1, - jsonrpc: '2.0' as const, - method: 'test', - }; - - await expect(async () => provider.request(exampleRequest)).rejects.toThrow( - 'Test error', - ); - }); -}); diff --git a/packages/eth-json-rpc-provider/src/provider-from-engine.ts b/packages/eth-json-rpc-provider/src/provider-from-engine.ts deleted file mode 100644 index 0f90662507d..00000000000 --- a/packages/eth-json-rpc-provider/src/provider-from-engine.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { JsonRpcEngine } from '@metamask/json-rpc-engine'; - -import { InternalProvider } from './internal-provider'; - -/** - * Construct an Ethereum provider from the given JSON-RPC engine. - * - * @param engine - The JSON-RPC engine to construct a provider from. - * @returns An Ethereum provider. - */ -export function providerFromEngine(engine: JsonRpcEngine): InternalProvider { - return new InternalProvider({ engine }); -} diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.test.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.test.ts index c3e73d31530..6c20713e0bf 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.test.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.test.ts @@ -1,13 +1,21 @@ -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware as LegacyJsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; import { providerErrors } from '@metamask/rpc-errors'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; -import { providerFromMiddleware } from './provider-from-middleware'; +import { + providerFromMiddleware, + providerFromMiddlewareV2, +} from './provider-from-middleware'; describe('providerFromMiddleware', () => { it('handle a successful request', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const middleware: JsonRpcMiddleware = (_req, res, _next, end) => { + const middleware: LegacyJsonRpcMiddleware = ( + _req, + res, + _next, + end, + ) => { res.result = 42; end(); }; @@ -43,3 +51,35 @@ describe('providerFromMiddleware', () => { ); }); }); + +describe('providerFromMiddlewareV2', () => { + it('handle a successful request', async () => { + const middleware: JsonRpcMiddleware = () => 42; + const provider = providerFromMiddlewareV2(middleware); + const exampleRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + const response = await provider.request(exampleRequest); + + expect(response).toBe(42); + }); + + it('handle a failed request', async () => { + const middleware: JsonRpcMiddleware = () => { + throw new Error('Test error'); + }; + const provider = providerFromMiddlewareV2(middleware); + const exampleRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + await expect(async () => provider.request(exampleRequest)).rejects.toThrow( + 'Test error', + ); + }); +}); diff --git a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts index e77d4874644..33905a62c86 100644 --- a/packages/eth-json-rpc-provider/src/provider-from-middleware.ts +++ b/packages/eth-json-rpc-provider/src/provider-from-middleware.ts @@ -1,22 +1,50 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcParams } from '@metamask/utils'; +import { asV2Middleware } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware as LegacyJsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import type { + JsonRpcMiddleware, + ResultConstraint, +} from '@metamask/json-rpc-engine/v2'; +import { + JsonRpcEngineV2, + type ContextConstraint, +} from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; -import type { InternalProvider } from './internal-provider'; -import { providerFromEngine } from './provider-from-engine'; +import { InternalProvider } from './internal-provider'; /** * Construct an Ethereum provider from the given middleware. * * @param middleware - The middleware to construct a provider from. * @returns An Ethereum provider. + * @deprecated Use `JsonRpcEngineV2` middleware and {@link providerFromMiddlewareV2} instead. */ export function providerFromMiddleware< Params extends JsonRpcParams, Result extends Json, ->(middleware: JsonRpcMiddleware): InternalProvider { - const engine: JsonRpcEngine = new JsonRpcEngine(); - engine.push(middleware); - const provider: InternalProvider = providerFromEngine(engine); - return provider; +>(middleware: LegacyJsonRpcMiddleware): InternalProvider { + return providerFromMiddlewareV2( + asV2Middleware(middleware) as JsonRpcMiddleware, + ); +} + +/** + * Construct an Ethereum provider from the given middleware. + * + * @param middleware - The middleware to construct a provider from. + * @returns An Ethereum provider. + */ +export function providerFromMiddlewareV2< + Request extends JsonRpcRequest, + Middleware extends JsonRpcMiddleware< + Request, + ResultConstraint, + ContextConstraint + >, +>(middleware: Middleware): InternalProvider { + return new InternalProvider({ + engine: JsonRpcEngineV2.create({ + middleware: [middleware as JsonRpcMiddleware], + }), + }); } diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index da4d5238af1..2f2d9c57f0a 100644 --- a/packages/json-rpc-engine/CHANGELOG.md +++ b/packages/json-rpc-engine/CHANGELOG.md @@ -9,9 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7032](https://github.com/MetaMask/core/pull/7032)) - - This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation. - See the readme for details. +- Add `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7001](https://github.com/MetaMask/core/pull/7001), [#7032](https://github.com/MetaMask/core/pull/7032)) + - This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation. See the readme for details. ## [10.1.1] diff --git a/packages/json-rpc-engine/src/asV2Middleware.test.ts b/packages/json-rpc-engine/src/asV2Middleware.test.ts index d268b4cf3f9..b91ef4c557b 100644 --- a/packages/json-rpc-engine/src/asV2Middleware.test.ts +++ b/packages/json-rpc-engine/src/asV2Middleware.test.ts @@ -171,6 +171,21 @@ describe('asV2Middleware', () => { ); }); + it('does not forward undefined errors from legacy middleware', async () => { + const legacyMiddleware = jest.fn((_req, res, _next, end) => { + res.error = undefined; + res.result = 42; + end(); + }); + + const v2Engine = JsonRpcEngineV2.create({ + middleware: [asV2Middleware(legacyMiddleware)], + }); + + const result = await v2Engine.handle(makeRequest()); + expect(result).toBe(42); + }); + it('allows v2 engine to continue when legacy middleware does not end', async () => { const legacyMiddleware = jest.fn((_req, _res, next) => { next(); diff --git a/packages/json-rpc-engine/src/asV2Middleware.ts b/packages/json-rpc-engine/src/asV2Middleware.ts index 34f4bd34877..14333cbb4e9 100644 --- a/packages/json-rpc-engine/src/asV2Middleware.ts +++ b/packages/json-rpc-engine/src/asV2Middleware.ts @@ -19,7 +19,7 @@ import { fromLegacyRequest, propagateToContext, propagateToRequest, - unserializeError, + deserializeError, } from './v2/compatibility-utils'; import type { // Used in docs. @@ -49,8 +49,9 @@ export function asV2Middleware< export function asV2Middleware< Params extends JsonRpcParams, Request extends JsonRpcRequest, + Result extends Json, >( - ...middleware: LegacyMiddleware[] + ...middleware: LegacyMiddleware[] ): JsonRpcMiddleware; /** @@ -69,7 +70,9 @@ export function asV2Middleware< ): JsonRpcMiddleware { const legacyMiddleware = typeof engineOrMiddleware === 'function' - ? mergeMiddleware([engineOrMiddleware, ...rest]) + ? // mergeMiddleware uses .asMiddleware() internally, which is necessary for our purposes. + // See comment on this below. + mergeMiddleware([engineOrMiddleware, ...rest]) : engineOrMiddleware.asMiddleware(); return async ({ request, context, next }) => { @@ -101,8 +104,11 @@ export function asV2Middleware< }); propagateToContext(req, context); - if (hasProperty(response, 'error')) { - throw unserializeError(response.error); + // Mimic the behavior of JsonRpcEngine.#handle(), which only treats truthy errors as errors. + // Legacy middleware may violate the invariant that response objects have either a result or an + // error property. In practice, we may see response objects with results and `{ error: undefined }`. + if (hasProperty(response, 'error') && response.error) { + throw deserializeError(response.error); } else if (hasProperty(response, 'result')) { return response.result as ResultConstraint; } diff --git a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts index fe89e800ef0..a6c4dce794a 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts @@ -73,6 +73,9 @@ type ConstructorOptions< >; }; +/** + * The request type of a middleware. + */ export type RequestOf = Middleware extends JsonRpcMiddleware< infer Request, @@ -91,11 +94,23 @@ type ContextOf = ? C : never; -export type MergedContextOf< - // Non-polluting `any` constraint. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Middleware extends JsonRpcMiddleware, -> = MergeContexts>; +/** + * A constraint for {@link JsonRpcMiddleware} generic parameters. + */ +// Non-polluting `any` constraint. +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type MiddlewareConstraint = JsonRpcMiddleware< + any, + ResultConstraint, + MiddlewareContext +>; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +/** + * The context supertype of a middleware type. + */ +export type MergedContextOf = + MergeContexts>; const INVALID_ENGINE = Symbol('Invalid engine'); diff --git a/packages/json-rpc-engine/src/v2/JsonRpcServer.ts b/packages/json-rpc-engine/src/v2/JsonRpcServer.ts index bc0bbd0ed62..dbf782aec64 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcServer.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcServer.ts @@ -11,8 +11,8 @@ import { hasProperty, isObject } from '@metamask/utils'; import type { JsonRpcMiddleware, MergedContextOf, + MiddlewareConstraint, RequestOf, - ResultConstraint, } from './JsonRpcEngineV2'; import { JsonRpcEngineV2 } from './JsonRpcEngineV2'; import type { JsonRpcCall } from './utils'; @@ -20,7 +20,7 @@ import { getUniqueId } from '../getUniqueId'; type OnError = (error: unknown) => void; -type Options = { +type Options = { onError?: OnError; } & ( | { @@ -58,14 +58,7 @@ const jsonrpc = '2.0' as const; * ``` */ export class JsonRpcServer< - Middleware extends JsonRpcMiddleware< - // Non-polluting `any` constraint. - /* eslint-disable @typescript-eslint/no-explicit-any */ - any, - ResultConstraint, - any - /* eslint-enable @typescript-eslint/no-explicit-any */ - > = JsonRpcMiddleware, + Middleware extends MiddlewareConstraint = JsonRpcMiddleware, > { readonly #engine: JsonRpcEngineV2< RequestOf, diff --git a/packages/json-rpc-engine/src/v2/MiddlewareContext.ts b/packages/json-rpc-engine/src/v2/MiddlewareContext.ts index 8eb291dcc7b..073b7e0c760 100644 --- a/packages/json-rpc-engine/src/v2/MiddlewareContext.ts +++ b/packages/json-rpc-engine/src/v2/MiddlewareContext.ts @@ -122,6 +122,9 @@ export type MergeContexts = ExcludeNever>>> >; +/** + * A constraint for {@link MiddlewareContext} generic parameters. + */ // Non-polluting `any` constraint. // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ContextConstraint = MiddlewareContext; diff --git a/packages/json-rpc-engine/src/v2/compatibility-utils.test.ts b/packages/json-rpc-engine/src/v2/compatibility-utils.test.ts index 9e75753daa3..8e26b3ede9f 100644 --- a/packages/json-rpc-engine/src/v2/compatibility-utils.test.ts +++ b/packages/json-rpc-engine/src/v2/compatibility-utils.test.ts @@ -7,7 +7,7 @@ import { makeContext, propagateToContext, propagateToRequest, - unserializeError, + deserializeError, } from './compatibility-utils'; import { MiddlewareContext } from './MiddlewareContext'; import { stringify } from './utils'; @@ -348,7 +348,7 @@ describe('compatibility-utils', () => { }); }); - describe('unserializeError', () => { + describe('deserializeError', () => { // Requires some special handling due to the possible existence or // non-existence of Error.isError describe('Error.isError', () => { @@ -382,7 +382,7 @@ describe('compatibility-utils', () => { isError.mockReturnValueOnce(true); const originalError = new Error('test error'); - const result = unserializeError(originalError); + const result = deserializeError(originalError); expect(result).toBe(originalError); }); @@ -390,14 +390,14 @@ describe('compatibility-utils', () => { isError.mockReturnValueOnce(false); const originalError = new Error('test error'); - const result = unserializeError(originalError); + const result = deserializeError(originalError); expect(result).toBe(originalError); }); }); it('creates a new Error when thrown value is a string', () => { const errorMessage = 'test error message'; - const result = unserializeError(errorMessage); + const result = deserializeError(errorMessage); expect(result).toBeInstanceOf(Error); expect(result.message).toBe(errorMessage); @@ -406,7 +406,7 @@ describe('compatibility-utils', () => { it.each([42, true, false, null, undefined, Symbol('test')])( 'creates a new Error with stringified message for non-object values', (value) => { - const result = unserializeError(value); + const result = deserializeError(value); expect(result).toBeInstanceOf(Error); expect(result.message).toBe(`Unknown error: ${stringify(value)}`); @@ -421,7 +421,7 @@ describe('compatibility-utils', () => { data: { foo: 'bar' }, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result).toBeInstanceOf(JsonRpcError); expect(result).toMatchObject({ @@ -439,7 +439,7 @@ describe('compatibility-utils', () => { data: { foo: 'bar' }, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result).toBeInstanceOf(Error); expect(result).not.toBeInstanceOf(JsonRpcError); @@ -455,7 +455,7 @@ describe('compatibility-utils', () => { message: 'test error message', }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result).toBeInstanceOf(Error); expect(result).not.toBeInstanceOf(JsonRpcError); @@ -469,7 +469,7 @@ describe('compatibility-utils', () => { stack: stackTrace, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result).toBeInstanceOf(Error); expect(result.stack).toBe(stackTrace); @@ -485,7 +485,7 @@ describe('compatibility-utils', () => { data, }; - const result = unserializeError(thrownValue) as JsonRpcError; + const result = deserializeError(thrownValue) as JsonRpcError; expect(result.cause).toBe(cause); expect(result.data).toStrictEqual({ @@ -499,7 +499,7 @@ describe('compatibility-utils', () => { code: 1234, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result.message).toBe('Unknown error'); }); @@ -510,7 +510,7 @@ describe('compatibility-utils', () => { message: 42, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result.message).toBe('Unknown error'); }); @@ -521,7 +521,7 @@ describe('compatibility-utils', () => { message: 42, }; - const result = unserializeError(thrownValue); + const result = deserializeError(thrownValue); expect(result.message).toBe('Internal JSON-RPC error.'); }); diff --git a/packages/json-rpc-engine/src/v2/compatibility-utils.ts b/packages/json-rpc-engine/src/v2/compatibility-utils.ts index 03257b6a2ef..cdd65cb0de4 100644 --- a/packages/json-rpc-engine/src/v2/compatibility-utils.ts +++ b/packages/json-rpc-engine/src/v2/compatibility-utils.ts @@ -135,7 +135,7 @@ export function propagateToRequest( * @param thrown - The thrown value to unserialize. * @returns The unserialized error. */ -export function unserializeError(thrown: unknown): Error | JsonRpcError { +export function deserializeError(thrown: unknown): Error | JsonRpcError { // @ts-expect-error - New, but preferred if available. // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError if (typeof Error.isError === 'function' && Error.isError(thrown)) { diff --git a/packages/json-rpc-engine/src/v2/index.ts b/packages/json-rpc-engine/src/v2/index.ts index 29560da7df6..ba2c932e428 100644 --- a/packages/json-rpc-engine/src/v2/index.ts +++ b/packages/json-rpc-engine/src/v2/index.ts @@ -4,13 +4,16 @@ export { createScaffoldMiddleware } from './createScaffoldMiddleware'; export { JsonRpcEngineV2 } from './JsonRpcEngineV2'; export type { JsonRpcMiddleware, + MergedContextOf, MiddlewareParams, + MiddlewareConstraint, Next, + RequestOf, ResultConstraint, } from './JsonRpcEngineV2'; export { JsonRpcServer } from './JsonRpcServer'; export { MiddlewareContext } from './MiddlewareContext'; -export type { EmptyContext } from './MiddlewareContext'; +export type { EmptyContext, ContextConstraint } from './MiddlewareContext'; export { isNotification, isRequest, JsonRpcEngineError } from './utils'; export type { Json, diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 879bc61d255..7d7021afa16 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Use `InternalProvider` instead of `SafeEventEmitterProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - Providers accessible either via network clients or global proxies no longer emit events (or inherit from EventEmitter, for that matter). +- **BREAKING:** Migrate `NetworkClient` to `JsonRpcEngineV2` ([#6976](https://github.com/MetaMask/core/pull/6976)) + - This ought to be unobservable, but we mark it as breaking out of an abundance of caution. +- **BREAKING:** Stop retrying `undefined` results for methods that include a block tag parameter ([#7001](https://github.com/MetaMask/core/pull/7001)) + - The network client middleware, via `@metamask/eth-json-rpc-middleware`, will now throw an error if it encounters an + `undefined` result when dispatching a request with a later block number than the originally requested block number. + - In practice, this should happen rarely if ever. - Bump `@metamask/controller-utils` from `^11.14.1` to `^11.15.0` ([#7003](https://github.com/MetaMask/core/pull/7003)) ### Fixed 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 515c625816c..84b5a373be2 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -12,19 +12,18 @@ import { createFetchMiddleware, createRetryOnEmptyMiddleware, } from '@metamask/eth-json-rpc-middleware'; -import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; +import { InternalProvider } from '@metamask/eth-json-rpc-provider'; +import { providerFromMiddlewareV2 } from '@metamask/eth-json-rpc-provider'; +import { asV2Middleware } from '@metamask/json-rpc-engine'; import { - providerFromEngine, - 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'; @@ -50,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. * @@ -137,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, @@ -170,11 +179,13 @@ export function createNetworkClient({ rpcApiMiddleware, }); - const engine = new JsonRpcEngine(); - - engine.push(networkMiddleware); - - const provider = providerFromEngine(engine); + const provider = new InternalProvider({ + engine: JsonRpcEngineV2.create({ + middleware: [ + networkMiddleware as unknown as JsonRpcMiddleware, + ], + }), + }); const destroy = () => { // TODO: Either fix this lint violation or explain why it's necessary to ignore. @@ -241,17 +252,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(); } /** @@ -273,11 +286,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(); }; @@ -299,21 +311,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(); } /** @@ -322,11 +336,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 77be127ecbd..63c52992264 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -16802,6 +16802,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 ${ diff --git a/packages/network-controller/tests/network-client/block-hash-in-response.ts b/packages/network-controller/tests/network-client/block-hash-in-response.ts index 0bfae7d9a7e..a3f3e379de9 100644 --- a/packages/network-controller/tests/network-client/block-hash-in-response.ts +++ b/packages/network-controller/tests/network-client/block-hash-in-response.ts @@ -203,7 +203,7 @@ export function testsForRpcMethodsThatCheckForBlockHashInResponse( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method }; const mockResult = emptyValue; diff --git a/packages/network-controller/tests/network-client/block-param.ts b/packages/network-controller/tests/network-client/block-param.ts index 6492ab9b381..f71326c6bde 100644 --- a/packages/network-controller/tests/network-client/block-param.ts +++ b/packages/network-controller/tests/network-client/block-param.ts @@ -209,7 +209,7 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method, @@ -1227,7 +1227,7 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method, @@ -1360,7 +1360,7 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { if (providerType === 'infura') { it(`retries up to 10 times if a "${emptyValue}" response is returned, returning successful non-empty response if there is one on the 10th try`, async () => { const request = { @@ -1555,7 +1555,7 @@ export function testsForRpcMethodSupportingBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method, diff --git a/packages/network-controller/tests/network-client/no-block-param.ts b/packages/network-controller/tests/network-client/no-block-param.ts index 97b6cbd10e2..d79f4357f01 100644 --- a/packages/network-controller/tests/network-client/no-block-param.ts +++ b/packages/network-controller/tests/network-client/no-block-param.ts @@ -149,7 +149,7 @@ export function testsForRpcMethodAssumingNoBlockParam( }); }); - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { + for (const emptyValue of [null, '\u003cnil\u003e']) { it(`does not retry an empty response of "${emptyValue}"`, async () => { const request = { method }; const mockResult = emptyValue; diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 8d0d5aee5ed..b02edfcb6b4 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -13,6 +13,7 @@ * `jest.config.packages.js`. */ "paths": { + "@metamask/json-rpc-engine/v2": ["../json-rpc-engine/src/v2/index.ts"], "@metamask/*": ["../*/src"] }, "strict": true, diff --git a/yarn.lock b/yarn.lock index af6dfe11497..f3537f80cf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3612,10 +3612,10 @@ __metadata: ethers: "npm:^6.12.0" jest: "npm:^27.5.1" jest-it-up: "npm:^2.0.2" + nanoid: "npm:^3.3.8" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" typescript: "npm:~5.2.2" - uuid: "npm:^8.3.2" languageName: unknown linkType: soft