diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index a1f91779e21..5201510858d 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -115,84 +115,6 @@ "packages/eth-block-tracker/tests/withBlockTracker.ts": { "@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 - }, - "packages/eth-json-rpc-middleware/src/block-ref-rewrite.test.ts": { - "@typescript-eslint/no-misused-promises": 3 - }, - "packages/eth-json-rpc-middleware/src/block-ref-rewrite.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/block-ref.test.ts": { - "jest/expect-expect": 2 - }, - "packages/eth-json-rpc-middleware/src/block-ref.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/block-tracker-inspector.ts": { - "jsdoc/match-description": 1, - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/fetch.test.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/fetch.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/inflight-cache.ts": { - "@typescript-eslint/no-explicit-any": 1, - "jsdoc/require-jsdoc": 4 - }, - "packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/retryOnEmpty.test.ts": { - "jsdoc/match-description": 3 - }, - "packages/eth-json-rpc-middleware/src/retryOnEmpty.ts": { - "jsdoc/match-description": 2 - }, - "packages/eth-json-rpc-middleware/src/utils/cache.ts": { - "jsdoc/match-description": 6 - }, - "packages/eth-json-rpc-middleware/src/utils/common.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/utils/error.ts": { - "jsdoc/require-jsdoc": 1 - }, - "packages/eth-json-rpc-middleware/src/utils/normalize.ts": { - "jsdoc/match-description": 3 - }, - "packages/eth-json-rpc-middleware/src/utils/timeout.ts": { - "jsdoc/match-description": 1 - }, - "packages/eth-json-rpc-middleware/src/utils/validation.ts": { - "jsdoc/require-jsdoc": 4 - }, - "packages/eth-json-rpc-middleware/src/wallet.ts": { - "@typescript-eslint/no-explicit-any": 2, - "@typescript-eslint/prefer-nullish-coalescing": 5, - "jsdoc/match-description": 3, - "jsdoc/require-jsdoc": 12 - }, - "packages/eth-json-rpc-middleware/test/setupAfterEnv.ts": { - "@typescript-eslint/no-explicit-any": 3, - "jsdoc/match-description": 2 - }, - "packages/eth-json-rpc-middleware/test/util/helpers.ts": { - "@typescript-eslint/no-explicit-any": 5, - "jsdoc/match-description": 11 - }, "packages/gas-fee-controller/src/GasFeeController.test.ts": { "import-x/namespace": 2 }, @@ -219,7 +141,7 @@ "jsdoc/check-tag-names": 1 }, "packages/message-manager/src/AbstractMessageManager.ts": { - "jsdoc/check-tag-names": 25 + "jsdoc/check-tag-names": 23 }, "packages/message-manager/src/DecryptMessageManager.ts": { "jsdoc/check-tag-names": 11 @@ -236,9 +158,6 @@ "packages/name-controller/src/util.ts": { "jsdoc/require-returns": 1 }, - "packages/network-controller/src/NetworkController.ts": { - "@typescript-eslint/no-misused-promises": 1 - }, "packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts": { "@typescript-eslint/no-misused-promises": 1 }, diff --git a/eslint.config.mjs b/eslint.config.mjs index 5d857eb6c20..afe2d00336b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -230,17 +230,6 @@ const config = createConfig([ '@typescript-eslint/consistent-type-definitions': 'warn', }, }, - { - files: ['packages/eth-json-rpc-middleware/**/*.ts'], - rules: { - // TODO: Re-enable these rules or add inline ignores for warranted cases - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/prefer-nullish-coalescing': 'warn', - 'jsdoc/match-description': 'warn', - 'jsdoc/require-jsdoc': 'warn', - 'no-restricted-syntax': 'warn', - }, - }, { files: ['packages/foundryup/**/*.{js,ts}'], rules: { diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index 673180d1897..893803340bb 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Migrate to `JsonRpcEngineV2` ([#7065](https://github.com/MetaMask/core/pull/7065)) + - Migrates all middleware from `JsonRpcEngine` to `JsonRpcEngineV2`. + - Signatures of various middleware dependencies, e.g. `processTransaction` of `createWalletMiddleware`, have changed + and must be updated by consumers. + - Be advised that request objects are now deeply frozen, and cannot be mutated. + - To continue using this package with the legacy `JsonRpcEngine`, use the `asLegacyMiddleware` backwards compatibility function. - **BREAKING:** Use `InternalProvider` instead of `SafeEventEmitterProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - Wherever a `SafeEventEmitterProvider` was expected, an `InternalProvider` is now expected instead. - **BREAKING:** Stop retrying `undefined` results for methods that include a block tag parameter ([#7001](https://github.com/MetaMask/core/pull/7001)) diff --git a/packages/eth-json-rpc-middleware/jest.config.js b/packages/eth-json-rpc-middleware/jest.config.js index 5d557d1b1cb..4c500d749cb 100644 --- a/packages/eth-json-rpc-middleware/jest.config.js +++ b/packages/eth-json-rpc-middleware/jest.config.js @@ -20,7 +20,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 81.85, + branches: 81.49, functions: 90.66, lines: 89.13, statements: 89.2, diff --git a/packages/eth-json-rpc-middleware/package.json b/packages/eth-json-rpc-middleware/package.json index 023f305f75c..06faf9d08cd 100644 --- a/packages/eth-json-rpc-middleware/package.json +++ b/packages/eth-json-rpc-middleware/package.json @@ -59,6 +59,7 @@ "@metamask/eth-json-rpc-provider": "^5.0.1", "@metamask/eth-sig-util": "^8.2.0", "@metamask/json-rpc-engine": "^10.1.1", + "@metamask/message-manager": "^14.0.0", "@metamask/rpc-errors": "^7.0.2", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.8.1", @@ -71,8 +72,10 @@ "@metamask/error-reporting-service": "^3.0.0", "@metamask/network-controller": "^25.0.0", "@ts-bridge/cli": "^0.6.4", + "@types/deep-freeze-strict": "^1.1.0", "@types/jest": "^27.4.1", "@types/pify": "^5.0.2", + "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", "tsd": "^0.31.2", diff --git a/packages/eth-json-rpc-middleware/src/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..da788362d6e 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; @@ -32,19 +33,19 @@ type BlockCacheMiddlewareOptions = { // class BlockCacheStrategy { - private cache: Cache; + #cache: Cache; constructor() { - this.cache = {}; + this.#cache = {}; } getBlockCache(blockNumberHex: string): BlockCache { const blockNumber: number = Number.parseInt(blockNumberHex, 16); - let blockCache: BlockCache = this.cache[blockNumber]; + let blockCache: BlockCache = this.#cache[blockNumber]; // create new cache if necesary if (!blockCache) { const newCache: BlockCache = {}; - this.cache[blockNumber] = newCache; + this.#cache[blockNumber] = newCache; blockCache = newCache; } return blockCache; @@ -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; } @@ -125,27 +126,33 @@ class BlockCacheStrategy { clearBefore(oldBlockHex: string): void { const oldBlockNumber: number = Number.parseInt(oldBlockHex, 16); // clear old caches - Object.keys(this.cache) + Object.keys(this.#cache) .map(Number) .filter((num) => num < oldBlockNumber) - .forEach((num) => delete this.cache[num]); + .forEach((num) => delete this.#cache[num]); } } +/** + * Creates a middleware that caches block-related requests. + * + * @param options - The options for the middleware. + * @param options.blockTracker - The block tracker to use. + * @returns The block cache middleware. + */ export function createBlockCacheMiddleware({ blockTracker, -}: BlockCacheMiddlewareOptions = {}): 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 +161,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..e5f5406c546 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({ @@ -205,6 +196,8 @@ describe('createBlockRefMiddleware', () => { describe.each(['earliest', 'pending', '0x200'])( 'if the block param is something other than "latest", like %o', (blockParam) => { + // Using custom expect helper + // eslint-disable-next-line jest/expect-expect it('does not make a direct request through the provider', async () => { const finalMiddleware = createFinalMiddlewareWithDefaultResult(); @@ -216,15 +209,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 +241,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), + }); }); }, ); @@ -278,6 +269,8 @@ describe('createBlockRefMiddleware', () => { }); describe('when the RPC method does not take a block parameter', () => { + // Using custom expect helper + // eslint-disable-next-line jest/expect-expect it('does not make a direct request through the provider', async () => { const finalMiddleware = createFinalMiddlewareWithDefaultResult(); @@ -289,12 +282,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 +311,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..917abbd560d 100644 --- a/packages/eth-json-rpc-middleware/src/fetch.test.ts +++ b/packages/eth-json-rpc-middleware/src/fetch.test.ts @@ -1,75 +1,68 @@ -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: [], - }); - - expect(requestSpy).toHaveBeenCalledWith( - { - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }, - { - headers: {}, - }, - ); - }); - - 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', - }, - }); + it.each([ + [undefined, undefined], + [undefined, 'somedapp.com'], + ['X-Dapp-Origin', undefined], + ['X-Dapp-Origin', 'somedapp.com'], + ])( + 'calls the RPC service with the correct request headers and body with originHttpHeaderKey="%s" and origin="%s"', + async (originHttpHeaderKey, origin) => { + const rpcService = createRpcService(); + const requestSpy = jest.spyOn(rpcService, 'request'); + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createFetchMiddleware({ + rpcService, + options: { + originHttpHeaderKey, + }, + }), + ], + }); - 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); - - expect(requestSpy).toHaveBeenCalledWith( - { - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }, - { - headers: { - 'X-Dapp-Origin': 'somedapp.com', + const context = new MiddlewareContext<{ origin: string }>( + // eslint-disable-next-line jest/no-conditional-in-test + origin ? { origin } : [], + ); + const expectedHeaders = + // eslint-disable-next-line jest/no-conditional-in-test + originHttpHeaderKey && origin ? { [originHttpHeaderKey]: origin } : {}; + + await engine.handle( + createRequest({ + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }), + { context }, + ); + + expect(requestSpy).toHaveBeenCalledWith( + { + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], }, - }, - ); - }); + { + headers: expectedHeaders, + }, + ); + }, + ); describe('if the response from the service does not contain an `error` field', () => { it('returns a successful JSON-RPC response containing the value of the `result` field', async () => { @@ -79,24 +72,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 +102,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 +147,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 +173,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 +183,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..bd5dc886a3d 100644 --- a/packages/eth-json-rpc-middleware/src/fetch.ts +++ b/packages/eth-json-rpc-middleware/src/fetch.ts @@ -1,19 +1,12 @@ -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 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 +27,31 @@ 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 } - : {}; - - const jsonRpcResponse = await rpcService.request( - { - id: req.id, - jsonrpc: req.jsonrpc, - method: req.method, - params: req.params, - }, - { - headers, - }, - ); +}): JsonRpcMiddleware< + JsonRpcRequest, + Json, + MiddlewareContext<{ origin: string }> +> { + return async ({ request, context }) => { + const origin = context.get('origin'); + const headers = + options.originHttpHeaderKey !== undefined && origin !== undefined + ? { [options.originHttpHeaderKey]: origin } + : {}; - // 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, - }); - } + const jsonRpcResponse = await rpcService.request(request, { + headers, + }); - // Discard the `id` and `jsonrpc` fields in the response body - // (the JSON-RPC engine will fill those in) - res.result = jsonRpcResponse.result; - }, - ); + // 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, + }); + } + 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..c488cc80bbf 100644 --- a/packages/eth-json-rpc-middleware/src/inflight-cache.ts +++ b/packages/eth-json-rpc-middleware/src/inflight-cache.ts @@ -1,112 +1,126 @@ -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 = [(result: Json) => void, (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(); - }); - return promise; - } + // 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, + ); + runRequestHandlers({ result }, activeRequestHandlers); + return result; + } catch (error) { + log( + 'Running %i collected handler(s) for failed request %o', + activeRequestHandlers.length, + request, + ); + runRequestHandlers({ error: error as Error }, activeRequestHandlers); + throw error; + } finally { + delete inflightRequests[cacheId]; + } + }; +} - function handleActiveRequest( - res: PendingJsonRpcResponse, - activeRequestHandlers: RequestHandlers[], - ): 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); - } - }); - }); - } +/** + * Creates a new request handler for the active request. + * + * @param activeRequestHandlers - The active request handlers. + * @returns A promise that resolves to the result of the request. + */ +function createActiveRequestHandler( + activeRequestHandlers: RequestHandler[], +): Promise { + const { resolve, promise, reject } = createDeferredPromise(); + activeRequestHandlers.push([ + (result: Json) => resolve(result), + (error: Error) => reject(error), + ]); + return promise; } -function deferredPromise() { - let resolve: any; - const promise: Promise = new Promise((_resolve) => { - resolve = _resolve; +/** + * Runs the request handlers for the given result or error. + * + * @param resultOrError - The result or error of the request. + * @param activeRequestHandlers - The active request handlers. + */ +function runRequestHandlers( + resultOrError: { result: Json } | { error: Error }, + activeRequestHandlers: RequestHandler[], +): void { + // use setTimeout so we can handle the original request first + setTimeout(() => { + activeRequestHandlers.forEach(([onSuccess, onError]) => { + if ('result' in resultOrError) { + onSuccess(resultOrError.result); + } else { + onError(resultOrError.error); + } + }); }); - return { resolve, promise }; } 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..71600b8c598 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts @@ -1,4 +1,4 @@ -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona'; import type { @@ -6,7 +6,8 @@ import type { RequestExecutionPermissionsRequestParams, RequestExecutionPermissionsResult, } from './wallet-request-execution-permissions'; -import { walletRequestExecutionPermissions } from './wallet-request-execution-permissions'; +import { createWalletRequestExecutionPermissionsHandler } from './wallet-request-execution-permissions'; +import type { WalletMiddlewareParams } from '../wallet'; const ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123a'; const CHAIN_ID_MOCK = '0x1'; @@ -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 WalletMiddlewareParams); }; 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 WalletMiddlewareParams), ).rejects.toThrow( `wallet_requestExecutionPermissions - no middleware configured`, ); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts index 62fdf2bb738..3ef8acfec99 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts @@ -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,11 +17,11 @@ import { type Hex, type Json, type JsonRpcRequest, - type PendingJsonRpcResponse, StrictHexStruct, } from '@metamask/utils'; import { validateParams } from '../utils/validation'; +import type { WalletMiddlewareContext } from '../wallet'; const PermissionStruct = object({ type: string(), @@ -66,24 +67,31 @@ 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', - ); - } +/** + * Creates a handler for the `wallet_requestExecutionPermissions` JSON-RPC method. + * + * @param options - The options for the handler. + * @param options.processRequestExecutionPermissions - The function to process the + * request execution permissions request. + * @returns A JSON-RPC middleware function that handles the + * `wallet_requestExecutionPermissions` JSON-RPC method. + */ +export function createWalletRequestExecutionPermissionsHandler({ + processRequestExecutionPermissions, +}: { + 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..a2d2fa2999c 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.test.ts @@ -1,11 +1,12 @@ -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +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'; +import type { WalletMiddlewareParams } from '../wallet'; 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 WalletMiddlewareParams); }; 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 WalletMiddlewareParams), ).rejects.toThrow( 'wallet_revokeExecutionPermission - no middleware configured', ); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts index b4343073a5c..5cf054646a5 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-revoke-execution-permission.ts @@ -1,13 +1,12 @@ +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'; +import type { WalletMiddlewareContext } from '../wallet'; export const RevokeExecutionPermissionResultStruct = object({}); @@ -28,24 +27,31 @@ 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); +/** + * Creates a handler for the `wallet_revokeExecutionPermission` JSON-RPC method. + * + * @param options - The options for the handler. + * @param options.processRevokeExecutionPermission - The function to process the + * revoke execution permission request. + * @returns A JSON-RPC middleware function that handles the + * `wallet_revokeExecutionPermission` JSON-RPC method. + */ +export function createWalletRevokeExecutionPermissionHandler({ + processRevokeExecutionPermission, +}: { + 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..3257ee3a824 --- /dev/null +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.test.ts @@ -0,0 +1,121 @@ +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 { + assertIsJsonRpcFailure, + assertIsJsonRpcSuccess, +} from '@metamask/utils'; + +import { + providerAsMiddleware, + providerAsMiddlewareV2, +} from './providerAsMiddleware'; +import { createRequest } from '../test/util/helpers'; + +const createMockProvider = (resultOrError: Json | Error): InternalProvider => + ({ + request: + resultOrError instanceof Error + ? jest.fn().mockRejectedValue(resultOrError) + : jest.fn().mockResolvedValue(resultOrError), + }) 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(); + assertIsJsonRpcSuccess(response); + expect(response.result).toStrictEqual(mockResult); + expect(mockProvider.request).toHaveBeenCalledWith(request); + + resolve(); + }); + }); + }); + + it('forwards errors to the provider and returns the error', async () => { + const mockError = new Error('test'); + const mockProvider = createMockProvider(mockError); + + const engine = new JsonRpcEngine(); + engine.push(providerAsMiddleware(mockProvider)); + + const request = createRequest({ + method: 'eth_chainId', + params: [], + }); + + await new Promise((resolve) => { + engine.handle(request, (error, response) => { + assertIsJsonRpcFailure(response); + expect(error).toBe(mockError); + expect(response.error).toStrictEqual( + expect.objectContaining({ + message: mockError.message, + code: -32603, + data: { + cause: { + message: mockError.message, + stack: expect.any(String), + }, + }, + }), + ); + expect(mockProvider.request).toHaveBeenCalledWith(request); + + resolve(); + }); + }); + }); +}); + +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); + }); + + it('forwards errors to the provider and returns the error', async () => { + const mockError = new Error('test'); + const mockProvider = createMockProvider(mockError); + + const engine = JsonRpcEngineV2.create({ + middleware: [providerAsMiddlewareV2(mockProvider)], + }); + + const request = createRequest({ + method: 'eth_chainId', + params: [], + }); + + await expect(engine.handle(request)).rejects.toThrow(mockError); + expect(mockProvider.request).toHaveBeenCalledWith(request); + }); +}); diff --git a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts index c070b191fae..2ce4ade7e31 100644 --- a/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts +++ b/packages/eth-json-rpc-middleware/src/providerAsMiddleware.ts @@ -1,14 +1,34 @@ 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'; +/** + * Creates a legacy JSON-RPC middleware that forwards requests to a provider. + * + * @param provider - The provider to forward requests to. + * @returns A legacy JSON-RPC middleware that forwards requests to the provider. + * @deprecated Use {@link providerAsMiddlewareV2} instead. + */ export function providerAsMiddleware( provider: InternalProvider, -): JsonRpcMiddleware { +): LegacyJsonRpcMiddleware { return createAsyncMiddleware(async (req, res) => { res.result = await provider.request(req); }); } + +/** + * Creates a V2 JSON-RPC middleware that forwards requests to a provider. + * + * @param provider - The provider to forward requests to. + * @returns A V2 JSON-RPC middleware that forwards requests to the provider. + */ +export function providerAsMiddlewareV2( + provider: InternalProvider, +): JsonRpcMiddleware { + 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 f9447a8dbc6..e227e3385cc 100644 --- a/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts +++ b/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts @@ -1,9 +1,8 @@ import type { PollingBlockTracker } from '@metamask/eth-block-tracker'; import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; -import type { Json, JsonRpcParams } from '@metamask/utils'; -import { klona } from 'klona/full'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +import { klona } from 'klona'; import { projectLogger, createModuleLogger } from './logging-utils'; import type { Block } from './types'; @@ -37,7 +36,7 @@ export function createRetryOnEmptyMiddleware({ }: { provider?: InternalProvider; blockTracker?: PollingBlockTracker; -} = {}): JsonRpcMiddleware { +} = {}): JsonRpcMiddleware { if (!provider) { throw Error( 'RetryOnEmptyMiddleware - mandatory "provider" option is missing.', @@ -50,16 +49,18 @@ export function createRetryOnEmptyMiddleware({ ); } - return createAsyncMiddleware(async (req, res, next) => { - const blockRefIndex: number | undefined = blockTagParamIndex(req.method); + return async ({ request, next }) => { + const blockRefIndex: number | undefined = blockTagParamIndex( + request.method, + ); // skip if method does not include blockRef if (blockRefIndex === undefined) { return next(); } // skip if not exact block references let blockRef: string | undefined = - Array.isArray(req.params) && req.params[blockRefIndex] - ? (req.params[blockRefIndex] as string) + Array.isArray(request.params) && request.params[blockRefIndex] + ? (request.params[blockRefIndex] as string) : undefined; // omitted blockRef implies "latest" if (blockRef === undefined) { @@ -98,7 +99,7 @@ export function createRetryOnEmptyMiddleware({ ); // create child request with specific block-ref - const childRequest = klona(req); + const childRequest = klona(request); // attempt child request until non-empty response is received const childResult = await retry(10, async () => { log('Performing request %o', childRequest); @@ -118,10 +119,8 @@ export function createRetryOnEmptyMiddleware({ return attemptResult; }); log('Copying result %o', childResult); - // copy child result onto original response - res.result = childResult; - return undefined; - }); + return childResult; + }; } /** diff --git a/packages/eth-json-rpc-middleware/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/utils/error.ts b/packages/eth-json-rpc-middleware/src/utils/error.ts index dbc693c0bf4..10da9b81163 100644 --- a/packages/eth-json-rpc-middleware/src/utils/error.ts +++ b/packages/eth-json-rpc-middleware/src/utils/error.ts @@ -2,6 +2,13 @@ import { errorCodes } from '@metamask/rpc-errors'; import { isJsonRpcError } from '@metamask/utils'; import type { JsonRpcError } from '@metamask/utils'; +/** + * Checks if a value is a JSON-RPC error that indicates an execution reverted error. + * + * @param error - The value to check. + * @returns True if the value is a JSON-RPC error that indicates an execution reverted + * error, false otherwise. + */ export function isExecutionRevertedError( error: unknown, ): error is JsonRpcError { diff --git a/packages/eth-json-rpc-middleware/src/utils/normalize.test.ts b/packages/eth-json-rpc-middleware/src/utils/normalize.test.ts index 98d64913f7e..046d30f8377 100644 --- a/packages/eth-json-rpc-middleware/src/utils/normalize.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/normalize.test.ts @@ -1,3 +1,6 @@ +import deepFreeze from 'deep-freeze-strict'; +import { klona } from 'klona'; + import { normalizeTypedMessage } from './normalize'; const MESSAGE_DATA_MOCK = { @@ -79,6 +82,19 @@ describe('normalizeTypedMessage', () => { ); }); + it('should normalize verifyingContract address in readonly data', () => { + const data = klona(MESSAGE_DATA_MOCK); + data.domain.verifyingContract = + '0Xae7ab96520de3a18e5e111b5eaab095312d7fe84'; + deepFreeze(data); + + const normalizedData = parseNormalizerResult(data); + + expect(normalizedData.domain.verifyingContract).toBe( + '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + ); + }); + it('should not modify if verifyingContract is already hexadecimal', () => { const expectedVerifyingContract = '0xae7ab96520de3a18e5e111b5eaab095312d7fe84'; diff --git a/packages/eth-json-rpc-middleware/src/utils/normalize.ts b/packages/eth-json-rpc-middleware/src/utils/normalize.ts index e7f2982aff5..55b37851891 100644 --- a/packages/eth-json-rpc-middleware/src/utils/normalize.ts +++ b/packages/eth-json-rpc-middleware/src/utils/normalize.ts @@ -32,9 +32,13 @@ export function normalizeTypedMessage(messageData: string) { return messageData; } - data.domain.verifyingContract = normalizeContractAddress(verifyingContract); - - return JSON.stringify(data); + return JSON.stringify({ + ...data, + domain: { + ...data.domain, + verifyingContract: normalizeContractAddress(verifyingContract), + }, + }); } /** diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts index d781790da1e..c833e4e1d9e 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts @@ -1,13 +1,14 @@ +import { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; import { providerErrors } from '@metamask/rpc-errors'; import type { StructError } from '@metamask/superstruct'; import { any, validate } from '@metamask/superstruct'; -import type { JsonRpcRequest } from '@metamask/utils'; import { resemblesAddress, validateAndNormalizeKeyholder, validateParams, } from './validation'; +import type { WalletMiddlewareKeyValues } from '../wallet'; jest.mock('@metamask/superstruct', () => ({ ...jest.requireActual('@metamask/superstruct'), @@ -15,7 +16,8 @@ jest.mock('@metamask/superstruct', () => ({ })); const ADDRESS_MOCK = '0xABCDabcdABCDabcdABCDabcdABCDabcdABCDabcd'; -const REQUEST_MOCK = {} as JsonRpcRequest; +const createContext = () => + new MiddlewareContext([['origin', 'test']]); const STRUCT_ERROR_MOCK = { failures: () => [ @@ -33,9 +35,7 @@ const STRUCT_ERROR_MOCK = { describe('Validation Utils', () => { const validateMock = jest.mocked(validate); - let getAccountsMock: jest.MockedFn< - (req: JsonRpcRequest) => Promise - >; + let getAccountsMock: jest.MockedFn<(origin: string) => Promise>; beforeEach(() => { jest.resetAllMocks(); @@ -47,7 +47,7 @@ describe('Validation Utils', () => { it('returns lowercase address', async () => { const result = await validateAndNormalizeKeyholder( ADDRESS_MOCK, - REQUEST_MOCK, + createContext(), { getAccounts: getAccountsMock, }, @@ -60,7 +60,7 @@ describe('Validation Utils', () => { getAccountsMock.mockResolvedValueOnce([]); await expect( - validateAndNormalizeKeyholder(ADDRESS_MOCK, REQUEST_MOCK, { + validateAndNormalizeKeyholder(ADDRESS_MOCK, createContext(), { getAccounts: getAccountsMock, }), ).rejects.toThrow(providerErrors.unauthorized()); @@ -68,7 +68,7 @@ describe('Validation Utils', () => { it('throws if address is not string', async () => { await expect( - validateAndNormalizeKeyholder(123 as never, REQUEST_MOCK, { + validateAndNormalizeKeyholder(123 as never, createContext(), { getAccounts: getAccountsMock, }), ).rejects.toThrow( @@ -78,7 +78,7 @@ describe('Validation Utils', () => { it('throws if address is empty string', async () => { await expect( - validateAndNormalizeKeyholder('' as never, REQUEST_MOCK, { + validateAndNormalizeKeyholder('' as never, createContext(), { getAccounts: getAccountsMock, }), ).rejects.toThrow( @@ -88,7 +88,7 @@ describe('Validation Utils', () => { it('throws if address length is not 40', async () => { await expect( - validateAndNormalizeKeyholder('0x123', REQUEST_MOCK, { + validateAndNormalizeKeyholder('0x123', createContext(), { getAccounts: getAccountsMock, }), ).rejects.toThrow( diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.ts b/packages/eth-json-rpc-middleware/src/utils/validation.ts index 0913574c4d7..9af85cd8a48 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.ts @@ -1,12 +1,25 @@ import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { Struct, StructError } from '@metamask/superstruct'; import { validate } from '@metamask/superstruct'; -import type { Hex, JsonRpcRequest } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; +import type { WalletMiddlewareContext } from '../wallet'; + +/** + * Validates and normalizes a keyholder address for transaction- and + * signature-related operations. + * + * @param address - The Ethereum address to validate and normalize. + * @param context - The context of the request. + * @param options - The options for the validation. + * @param options.getAccounts - The function to get the accounts for the origin. + * @returns The normalized address, if valid. Otherwise, throws + * an error + */ export async function validateAndNormalizeKeyholder( address: Hex, - req: JsonRpcRequest, - { getAccounts }: { getAccounts: (req: JsonRpcRequest) => Promise }, + context: WalletMiddlewareContext, + { getAccounts }: { getAccounts: (origin: string) => Promise }, ): Promise { if ( typeof address === 'string' && @@ -15,7 +28,7 @@ export async function validateAndNormalizeKeyholder( ) { // Ensure that an "unauthorized" error is thrown if the requester // does not have the `eth_accounts` permission. - const accounts = await getAccounts(req); + const accounts = await getAccounts(context.assertGet('origin')); const normalizedAccounts: string[] = accounts.map((_address) => _address.toLowerCase(), @@ -35,6 +48,14 @@ export async function validateAndNormalizeKeyholder( }); } +/** + * Validates the parameters of a request against a Superstruct schema. + * Throws a JSON-RPC error if the parameters are invalid. + * + * @param value - The value to validate. + * @param struct - The Superstruct schema to validate against. + * @throws An error if the parameters are invalid. + */ export function validateParams( value: unknown | ParamsType, struct: Struct, @@ -48,11 +69,24 @@ export function validateParams( } } +/** + * Checks if a string resembles an Ethereum address. + * + * @param str - The string to check. + * @returns True if the string resembles an Ethereum address, false otherwise. + */ export function resemblesAddress(str: string): boolean { // hex prefix 2 + 20 bytes return str.length === 2 + 20 * 2; } +/** + * Formats a Superstruct validation error into a human-readable string. + * + * @param error - The Superstruct validation error. + * @param message - The base error message to prepend to the formatted details. + * @returns The formatted error. + */ function formatValidationError(error: StructError, message: string): string { return `${message}\n\n${error .failures() diff --git a/packages/eth-json-rpc-middleware/src/wallet.test.ts b/packages/eth-json-rpc-middleware/src/wallet.test.ts index a92f1407d40..cac282209ff 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 { createHandleParams, 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( + ...createHandleParams({ + 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( + ...createHandleParams({ + 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( + ...createHandleParams({ + 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(...createHandleParams(payload)); expect(sendTxResult).toBeDefined(); expect(sendTxResult).toStrictEqual(testTxHash); expect(witnessedTxParams).toHaveLength(1); @@ -76,60 +87,79 @@ 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 payload = { method: 'eth_sendTransaction', params: [txParams] }; - const promise = pify(engine.handle).call(engine, payload); - await expect(promise).rejects.toThrow( + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); 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], + }; - const payload = { method: 'eth_sendTransaction', params: [txParams] }; - await pify(engine.handle).call(engine, payload); + await engine.handle(...createHandleParams(payload)); expect(witnessedTxParams).toHaveLength(1); expect(witnessedTxParams[0]).toStrictEqual(txParams); }); @@ -137,94 +167,96 @@ 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; - expect(sendTxResult).toBeDefined(); - expect(sendTxResult).toStrictEqual(testTxHash); + + expect(await engine.handle(...createHandleParams(payload))).toStrictEqual( + testTxHash, + ); expect(witnessedTxParams).toHaveLength(1); expect(witnessedTxParams[0]).toStrictEqual(txParams); }); 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(...createHandleParams(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(...createHandleParams(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); - await expect(promise).rejects.toThrow( + + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); @@ -232,15 +264,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 +287,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(...createHandleParams(payload)); expect(signMsgResult).toBeDefined(); expect(signMsgResult).toStrictEqual(testMsgSig); @@ -268,15 +301,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 +324,25 @@ describe('wallet', () => { method: 'eth_signTypedData', params: [message, '0x3d'], }; - await expect(pify(engine.handle).call(engine, payload)).rejects.toThrow( + await expect( + engine.handle(...createHandleParams(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', @@ -311,13 +350,14 @@ describe('wallet', () => { value: 'Hi, Alice!', }, ]; - const payload = { method: 'eth_signTypedData', params: [message, testUnkownAddress], }; - const promise = pify(engine.handle).call(engine, payload); - await expect(promise).rejects.toThrow( + + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); @@ -325,7 +365,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 +372,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 +407,9 @@ 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 = await engine.handle( + ...createHandleParams(payload), ); - const signTypedDataV3Result = signTypedDataV3Response.result; expect(signTypedDataV3Result).toBeDefined(); expect(signTypedDataV3Result).toStrictEqual(testMsgSig); @@ -385,7 +423,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 +430,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 +459,12 @@ describe('wallet', () => { params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup }; - const promise = pify(engine.handle).call(engine, payload); - await expect(promise).rejects.toThrow('Invalid input.'); + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow('Invalid input.'); }); it('should not throw if verifyingContract is undefined', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV3 = async (msgParams: TypedMessageParams) => { @@ -434,10 +472,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 +498,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(...createHandleParams(payload)); + expect(result).toBe( + '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c', + ); }); }); @@ -505,7 +540,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 +547,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(...createHandleParams(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 +572,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 +588,12 @@ describe('wallet', () => { ], }; - const promise = pify(engine.handle).call(engine, payload); - await expect(promise).rejects.toThrow('Invalid input.'); + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow('Invalid input.'); }); it('should not throw if request is permit with undefined value for verifyingContract address', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV4 = async (msgParams: TypedMessageParams) => { @@ -570,28 +601,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(...createHandleParams(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 +626,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(...createHandleParams(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 +651,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 +666,12 @@ describe('wallet', () => { ], }; - const promise = pify(engine.handle).call(engine, payload); - await expect(promise).rejects.toThrow('Invalid input.'); + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow('Invalid input.'); }); it('should throw if type of primaryType is not defined', async () => { - const engine = new JsonRpcEngine(); const getAccounts = async () => testAddresses.slice(); const witnessedMsgParams: TypedMessageParams[] = []; const processTypedMessageV4 = async (msgParams: TypedMessageParams) => { @@ -655,10 +679,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 +697,32 @@ describe('wallet', () => { ], }; - const promise = pify(engine.handle).call(engine, payload); - await expect(promise).rejects.toThrow('Invalid input.'); + await expect( + engine.handle(...createHandleParams(payload)), + ).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(...createHandleParams(payload)); expect(signMsgResult).toBeDefined(); expect(signMsgResult).toStrictEqual(testMsgSig); @@ -710,17 +735,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 +753,25 @@ describe('wallet', () => { params: [message, '0x3d'], }; - await expect(pify(engine.handle).call(engine, payload)).rejects.toThrow( + await expect( + engine.handle(...createHandleParams(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,8 +779,9 @@ describe('wallet', () => { params: [message, testUnkownAddress], }; - const promise = pify(engine.handle).call(engine, payload); - await expect(promise).rejects.toThrow( + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow( 'The requested account and/or method has not been authorized by the user.', ); }); @@ -771,15 +799,17 @@ 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( + ...createHandleParams(payload), + ); expect(ecrecoverResult).toBeDefined(); expect(ecrecoverResult).toStrictEqual(signParams.addressHex); }); @@ -797,15 +827,17 @@ 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( + ...createHandleParams(payload), + ); expect(ecrecoverResult).toBeDefined(); expect(ecrecoverResult).toStrictEqual(signParams.addressHex); }); diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index 944afe9e0e9..264e90ec5d0 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -1,27 +1,23 @@ 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, + MiddlewareContext, + MiddlewareParams, +} from '@metamask/json-rpc-engine/v2'; +import { createScaffoldMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { MessageRequest } from '@metamask/message-manager'; import { rpcErrors } from '@metamask/rpc-errors'; import { isValidHexAddress } from '@metamask/utils'; -import type { - JsonRpcRequest, - 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 +25,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; }; @@ -60,46 +43,86 @@ export type TypedMessageV1Params = Omit & { }; export type WalletMiddlewareOptions = { - getAccounts: (req: JsonRpcRequest) => Promise; + getAccounts: (origin: string) => Promise; processDecryptMessage?: ( msgParams: MessageParams, - req: JsonRpcRequest, + req: MessageRequest, ) => Promise; processEncryptionPublicKey?: ( address: string, - req: JsonRpcRequest, + req: MessageRequest, ) => Promise; processPersonalMessage?: ( msgParams: MessageParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, ) => Promise; processTransaction?: ( txParams: TransactionParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, ) => Promise; processSignTransaction?: ( txParams: TransactionParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, ) => Promise; processTypedMessage?: ( msgParams: TypedMessageV1Params, req: JsonRpcRequest, + context: WalletMiddlewareContext, version: string, ) => Promise; processTypedMessageV3?: ( msgParams: TypedMessageParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, version: string, ) => Promise; processTypedMessageV4?: ( msgParams: TypedMessageParams, req: JsonRpcRequest, + context: WalletMiddlewareContext, version: string, ) => Promise; processRequestExecutionPermissions?: ProcessRequestExecutionPermissionsHook; processRevokeExecutionPermission?: ProcessRevokeExecutionPermissionHook; }; +export type WalletMiddlewareKeyValues = { + networkClientId: string; + origin: string; + securityAlertResponse?: Record; + traceContext?: unknown; +}; + +export type WalletMiddlewareContext = + MiddlewareContext; + +export type WalletMiddlewareParams = MiddlewareParams< + JsonRpcRequest, + WalletMiddlewareContext +>; + +/** + * Creates a JSON-RPC middleware that handles "wallet"-related JSON-RPC methods. + * "Wallet" may have had a specific meaning at some point in the distant past, + * but at this point it's just an arbitrary label. + * + * @param options - The options for the middleware. + * @param options.getAccounts - The function to get the accounts for the origin. + * @param options.processDecryptMessage - The function to process the decrypt message request. + * @param options.processEncryptionPublicKey - The function to process the encryption public key request. + * @param options.processPersonalMessage - The function to process the personal message request. + * @param options.processTransaction - The function to process the transaction request. + * @param options.processSignTransaction - The function to process the sign transaction request. + * @param options.processTypedMessage - The function to process the typed message request. + * @param options.processTypedMessageV3 - The function to process the typed message v3 request. + * @param options.processTypedMessageV4 - The function to process the typed message v4 request. + * @param options.processRequestExecutionPermissions - The function to process the request execution permissions request. + * @param options.processRevokeExecutionPermission - The function to process the revoke execution permission request. + * @returns A JSON-RPC middleware that handles wallet-related JSON-RPC methods. + */ export function createWalletMiddleware({ getAccounts, processDecryptMessage, @@ -112,138 +135,175 @@ export function createWalletMiddleware({ processTypedMessageV4, processRequestExecutionPermissions, processRevokeExecutionPermission, -}: // }: WalletMiddlewareOptions): JsonRpcMiddleware { -WalletMiddlewareOptions): JsonRpcMiddleware { +}: WalletMiddlewareOptions): JsonRpcMiddleware< + JsonRpcRequest, + Json, + WalletMiddlewareContext +> { if (!getAccounts) { throw new Error('opts.getAccounts is required'); } - return createScaffoldMiddleware({ + 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); + /** + * Gets the accounts for the origin. + * + * @param options - Options bag. + * @param options.context - The context of the request. + * @returns The accounts for the origin. + */ + async function lookupAccounts({ + context, + }: WalletMiddlewareParams): Promise { + return await getAccounts(context.assertGet('origin')); } - async function lookupDefaultAccount( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { - const accounts = await getAccounts(req); - res.result = accounts[0] || null; + /** + * Gets the default account (i.e. first in the list) for the origin. + * + * @param options - Options bag. + * @param options.context - The context of the request. + * @returns The default account for the origin. + */ + async function lookupDefaultAccount({ + context, + }: WalletMiddlewareParams): Promise { + const accounts = await getAccounts(context.assertGet('origin')); + return accounts[0] || null; } // // transaction signatures // - async function sendTransaction( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + /** + * Sends a transaction. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The transaction hash. + */ + async function sendTransaction({ + request, + context, + }: WalletMiddlewareParams): 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 || '', context), }; - res.result = await processTransaction(txParams, req); + return await processTransaction(txParams, request, context); } - async function signTransaction( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + /** + * Signs a transaction. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed transaction. + */ + async function signTransaction({ + request, + context, + }: WalletMiddlewareParams): 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 || '', context), }; - res.result = await processSignTransaction(txParams, req); + return await processSignTransaction(txParams, request, context); } // // message signatures // - async function signTypedData( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + + /** + * Signs a `eth_signTypedData` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed message. + */ + async function signTypedData({ + request, + context, + }: WalletMiddlewareParams): 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], context); const version = 'V1'; const extraParams = params[2] || {}; const msgParams: TypedMessageV1Params = { @@ -254,27 +314,35 @@ WalletMiddlewareOptions): JsonRpcMiddleware { version, }; - res.result = await processTypedMessage(msgParams, req, version); + return await processTypedMessage(msgParams, request, context, version); } - async function signTypedDataV3( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + /** + * Signs a `eth_signTypedData_v3` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed message. + */ + async function signTypedDataV3({ + request, + context, + }: WalletMiddlewareParams): 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], context); const message = normalizeTypedMessage(params[1]); validatePrimaryType(message); validateVerifyingContract(message); @@ -286,27 +354,35 @@ WalletMiddlewareOptions): JsonRpcMiddleware { signatureMethod: 'eth_signTypedData_v3', }; - res.result = await processTypedMessageV3(msgParams, req, version); + return await processTypedMessageV3(msgParams, request, context, version); } - async function signTypedDataV4( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + /** + * Signs a `eth_signTypedData_v4` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed message. + */ + async function signTypedDataV4({ + request, + context, + }: WalletMiddlewareParams): 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], context); const message = normalizeTypedMessage(params[1]); validatePrimaryType(message); validateVerifyingContract(message); @@ -318,25 +394,33 @@ WalletMiddlewareOptions): JsonRpcMiddleware { signatureMethod: 'eth_signTypedData_v4', }; - res.result = await processTypedMessageV4(msgParams, req, version); + return await processTypedMessageV4(msgParams, request, context, version); } - async function personalSign( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + /** + * Signs a `personal_sign` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The signed message. + */ + async function personalSign({ + request, + context, + }: WalletMiddlewareParams): 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 +437,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, context); const msgParams: MessageParams = { ...extraParams, @@ -374,22 +452,28 @@ WalletMiddlewareOptions): JsonRpcMiddleware { signatureMethod: 'personal_sign', }; - res.result = await processPersonalMessage(msgParams, req); + return await processPersonalMessage(msgParams, request, context); } - async function personalRecover( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + /** + * Recovers the signer address from a `personal_sign` message. + * + * @param options - Options bag. + * @param options.request - The request. + * @returns The recovered signer address. + */ + async function personalRecover({ + request, + }: WalletMiddlewareParams): Promise { 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 +481,72 @@ WalletMiddlewareOptions): JsonRpcMiddleware { signature, }); - res.result = signerAddress; + return signerAddress; } - async function encryptionPublicKey( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + /** + * Gets the encryption public key for an address. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The encryption public key. + */ + async function encryptionPublicKey({ + request, + context, + }: WalletMiddlewareParams): 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], context); - res.result = await processEncryptionPublicKey(address, req); + return await processEncryptionPublicKey(address, { + id: request.id as string | number, + origin: context.assertGet('origin'), + securityAlertResponse: context.get('securityAlertResponse'), + }); } - async function decryptMessage( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - ): Promise { + /** + * Decrypts a message. + * + * @param options - Options bag. + * @param options.request - The request. + * @param options.context - The context of the request. + * @returns The decrypted message. + */ + async function decryptMessage({ + request, + context, + }: WalletMiddlewareParams): 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], + context, + ); const extraParams = params[2] || {}; const msgParams: MessageParams = { ...extraParams, @@ -447,7 +554,11 @@ WalletMiddlewareOptions): JsonRpcMiddleware { data: ciphertext, }; - res.result = await processDecryptMessage(msgParams, req); + return await processDecryptMessage(msgParams, { + id: request.id as string | number, + origin: context.assertGet('origin'), + securityAlertResponse: context.get('securityAlertResponse'), + }); } // @@ -459,15 +570,15 @@ WalletMiddlewareOptions): JsonRpcMiddleware { * copy of it. * * @param address - The address to validate and normalize. - * @param req - The request object. + * @param context - The context of the request. * @returns The normalized address, if valid. Otherwise, throws * an error */ async function validateAndNormalizeKeyholder( address: string, - req: JsonRpcRequest, + context: WalletMiddlewareContext, ): Promise { - return validateKeyholder(address as Hex, req, { getAccounts }); + return validateKeyholder(address as Hex, context, { getAccounts }); } } diff --git a/packages/eth-json-rpc-middleware/test/setupAfterEnv.ts b/packages/eth-json-rpc-middleware/test/setupAfterEnv.ts index 7608e43f298..9147c2fdf17 100644 --- a/packages/eth-json-rpc-middleware/test/setupAfterEnv.ts +++ b/packages/eth-json-rpc-middleware/test/setupAfterEnv.ts @@ -37,7 +37,7 @@ expect.extend({ * @param promise - The promise to test. * @returns The result of the matcher. */ - async toNeverResolve(promise: Promise) { + async toNeverResolve(promise: Promise) { if (this.isNot) { throw new Error( 'Using `.not.toNeverResolve(...)` is not supported. ' + @@ -46,8 +46,8 @@ expect.extend({ ); } - let resolutionValue: any; - let rejectionValue: any; + let resolutionValue: unknown; + let rejectionValue: unknown; try { resolutionValue = await Promise.race([ promise, @@ -67,8 +67,8 @@ expect.extend({ message: () => { return `Expected promise to never resolve after ${TIME_TO_WAIT_UNTIL_UNRESOLVED}ms, but it ${ rejectionValue - ? `was rejected with ${rejectionValue}` - : `resolved with ${resolutionValue}` + ? `was rejected with ${JSON.stringify(rejectionValue, null, 2)}` + : `resolved with ${JSON.stringify(resolutionValue, null, 2)}` }`; }, pass: false, diff --git a/packages/eth-json-rpc-middleware/test/util/helpers.ts b/packages/eth-json-rpc-middleware/test/util/helpers.ts index 65308688277..998481f9fdf 100644 --- a/packages/eth-json-rpc-middleware/test/util/helpers.ts +++ b/packages/eth-json-rpc-middleware/test/util/helpers.ts @@ -1,13 +1,17 @@ import { PollingBlockTracker } from '@metamask/eth-block-tracker'; import { InternalProvider } from '@metamask/eth-json-rpc-provider'; -import { - JsonRpcEngine, - type JsonRpcMiddleware, -} from '@metamask/json-rpc-engine'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; +import type { + JsonRpcMiddleware, + ResultConstraint, +} from '@metamask/json-rpc-engine/v2'; import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { klona } from 'klona/full'; import { isDeepStrictEqual } from 'util'; +import type { WalletMiddlewareKeyValues } from '../../src/wallet'; + export const createRequest = < Input extends Partial>, Output extends Input & JsonRpcRequest, @@ -18,11 +22,31 @@ export const createRequest = < jsonrpc: '2.0', id: request.id ?? '1', method: request.method ?? 'test_request', - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing params: request.params === undefined ? [] : request.params, } as Output; }; +const createHandleOptions = ( + keyValues: Partial = {}, +): { context: WalletMiddlewareKeyValues } => ({ + context: { + networkClientId: 'test-client-id', + origin: 'test-origin', + ...keyValues, + }, +}); + +export const createHandleParams = < + InputReq extends Partial>, + OutputReq extends InputReq & JsonRpcRequest, +>( + request: InputReq, + keyValues: Partial = {}, +): [OutputReq, ReturnType] => [ + createRequest(request), + createHandleOptions(keyValues), +]; + /** * An object that can be used to assign a canned result to a request made via * `provider.request`. @@ -66,22 +90,15 @@ export type ProviderRequestStub< * @template Result - The type that represents the result. * @returns The created middleware, as a mock function. */ -export function createFinalMiddlewareWithDefaultResult< - Params extends JsonRpcParams, - Result extends Json, ->(): JsonRpcMiddleware { - return jest.fn((req, res, _next, end) => { - if (res.id === undefined) { - res.id = req.id; +export function createFinalMiddlewareWithDefaultResult(): JsonRpcMiddleware { + return jest.fn(async ({ next }) => { + // Not a Node.js callback + // eslint-disable-next-line n/callback-return + const result = await next(); + if (result === undefined) { + return 'default result'; } - - res.jsonrpc ??= '2.0'; - - if (res.result === undefined) { - res.result = 'default result'; - } - - end(); + return result; }); } @@ -91,7 +108,10 @@ export function createFinalMiddlewareWithDefaultResult< * * @returns The provider and block tracker. */ -export function createProviderAndBlockTracker() { +export function createProviderAndBlockTracker(): { + provider: InternalProvider; + blockTracker: PollingBlockTracker; +} { const engine = new JsonRpcEngine(); const provider = new InternalProvider({ engine }); @@ -102,6 +122,10 @@ export function createProviderAndBlockTracker() { return { provider, blockTracker }; } +// An expedient for use with createEngine below. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyMiddleware = JsonRpcMiddleware, any>; + /** * Creates a JSON-RPC engine with the middleware under test and any * additional middleware. If no other middleware is provided, a final middleware @@ -112,29 +136,16 @@ export function createProviderAndBlockTracker() { * @returns The created engine. */ 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(); + middlewareUnderTest: AnyMiddleware, + ...otherMiddleware: AnyMiddleware[] +): JsonRpcEngineV2 { + return JsonRpcEngineV2.create({ + middleware: [ + middlewareUnderTest, + ...(otherMiddleware.length === 0 + ? [createFinalMiddlewareWithDefaultResult()] + : otherMiddleware), + ], }); } @@ -260,10 +271,10 @@ export function expectProviderRequestNotToHaveBeenMade( * @returns The Jest spy object that represents `provider.request` (so that * you can make assertions on the method later, if you like). */ -export function stubProviderRequests( - provider: InternalProvider, - stubs: ProviderRequestStub[], -) { +export function stubProviderRequests< + Params extends JsonRpcParams = JsonRpcParams, + Result extends Json = Json, +>(provider: InternalProvider, stubs: ProviderRequestStub[]) { const remainingStubs = klona(stubs); const callNumbersByRequest = new Map, number>(); return jest.spyOn(provider, 'request').mockImplementation(async (request) => { diff --git a/packages/eth-json-rpc-middleware/tsconfig.build.json b/packages/eth-json-rpc-middleware/tsconfig.build.json index f3452fcd990..8aacbb0393f 100644 --- a/packages/eth-json-rpc-middleware/tsconfig.build.json +++ b/packages/eth-json-rpc-middleware/tsconfig.build.json @@ -17,6 +17,9 @@ }, { "path": "../json-rpc-engine/tsconfig.build.json" + }, + { + "path": "../message-manager/tsconfig.build.json" } ], "include": ["../../types", "./src"], diff --git a/packages/eth-json-rpc-middleware/tsconfig.json b/packages/eth-json-rpc-middleware/tsconfig.json index a130e3632c4..240743caf93 100644 --- a/packages/eth-json-rpc-middleware/tsconfig.json +++ b/packages/eth-json-rpc-middleware/tsconfig.json @@ -18,7 +18,10 @@ }, { "path": "../network-controller" + }, + { + "path": "../message-manager" } ], - "include": ["../../types", "./src", "./test", "./types"] + "include": ["../../types", "./src", "./test"] } diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index ea26e0a1b9d..f5daa2b54f3 100644 --- a/packages/json-rpc-engine/CHANGELOG.md +++ b/packages/json-rpc-engine/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7032](https://github.com/MetaMask/core/pull/7032), [#7001](https://github.com/MetaMask/core/pull/7001), [#7061](https://github.com/MetaMask/core/pull/7061)) +- Add `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7032](https://github.com/MetaMask/core/pull/7032), [#7001](https://github.com/MetaMask/core/pull/7001), [#7061](https://github.com/MetaMask/core/pull/7061), [#7065](https://github.com/MetaMask/core/pull/7065)) - This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation. See the readme for details. ## [10.1.1] diff --git a/packages/json-rpc-engine/README.md b/packages/json-rpc-engine/README.md index ff10d2dc7f3..37edded5ed1 100644 --- a/packages/json-rpc-engine/README.md +++ b/packages/json-rpc-engine/README.md @@ -12,8 +12,10 @@ or ## Usage -> [!NOTE] +> [!TIP] > For the legacy `JsonRpcEngine`, see [its readme](./src/README.md). +> +> For how to migrate from the legacy `JsonRpcEngine` to `JsonRpcEngineV2`, see [Migrating from `JsonRpcEngine`](#migrating-from-jsonrpcengine). ```ts import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; @@ -784,6 +786,59 @@ to the response object via the `error` property. > appropriate request objects. `JsonRpcServer.handle()` will not type error at compile time if you attempt to pass > it an unsupported request object. +## Migrating from `JsonRpcEngine` + +Migrating from the legacy `JsonRpcEngine` to `JsonRpcEngineV2` is generally straightforward. +For an example, see [MetaMask/core#7065](https://github.com/MetaMask/core/pull/7065). +There are a couple of pitfalls to watch out for: + +### `MiddlewareContext` vs. non-JSON-RPC string properties + +The legacy `JsonRpcEngine` allowed non-JSON-RPC string properties to be attached to the request object. +`JsonRpcEngineV2` does not allow this, and instead you must use the `context` object to pass data between middleware. +While it's easy to migrate a middleware function body to use the `context` object, injected dependencies +of the middleware function may need to be updated. + +For example if you have a legacy middleware implementation like this: + +```ts +const createFooMiddleware = + (processFoo: (req: JsonRpcRequest) => string) => (req, res, next, end) => { + if (req.method === 'foo') { + const fooResult = processFoo(req); // May expect non-JSON-RPC properties on the request object! + res.result = fooResult; + end(); + } else { + next(); + } + }; +``` + +`processFoo` may expect non-JSON-RPC properties on the request object. To fully migrate the middleware, you need to +investigate the implementation of `processFoo` and potentially update it to accept a `context` object. + +### Frozen requests + +In the legacy `JsonRpcEngine`, request and response objects are mutable and shared between all middleware. +In `JsonRpcEngineV2`, response objects are not visible to middleware, and request objects are deeply frozen. +If injected dependencies mutate the request object, it will cause an error. + +For example, if you have a legacy middleware implementation like this: + +```ts +const createBarMiddleware = + (processBar: (req: JsonRpcRequest) => string) => (req, _res, next, _end) => { + if (req.method === 'bar') { + processBar(req); // May mutate the request object! + } + next(); + }; +``` + +`processBar` may mutate the request object. To fully migrate the middleware, you need to +investigate the implementation of `processBar` and update it to not directly mutate the request object. +See [Request modification](#request-modification) for how to modify the request object in `JsonRpcEngineV2`. + ## Contributing This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/json-rpc-engine/src/README.md b/packages/json-rpc-engine/src/README.md index 48be9407e05..b3d41fd979b 100644 --- a/packages/json-rpc-engine/src/README.md +++ b/packages/json-rpc-engine/src/README.md @@ -4,6 +4,11 @@ The deprecated, original `JsonRpcEngine` implementation. To be removed once the rest of MetaMask's codebase has been migrated to `JsonRpcEngineV2`. +> [!TIP] +> For the new `JsonRpcEngineV2`, see [the package readme](../README.md). +> +> For how to migrate from the legacy `JsonRpcEngine` to `JsonRpcEngineV2`, see [this package readme section](../README.md#migrating-from-jsonrpcengine). + ## Usage ```js diff --git a/packages/json-rpc-engine/src/asV2Middleware.ts b/packages/json-rpc-engine/src/asV2Middleware.ts index 14333cbb4e9..e66cd9b02ca 100644 --- a/packages/json-rpc-engine/src/asV2Middleware.ts +++ b/packages/json-rpc-engine/src/asV2Middleware.ts @@ -14,6 +14,7 @@ import type { } from './JsonRpcEngine'; import { type JsonRpcMiddleware as LegacyMiddleware } from './JsonRpcEngine'; import { mergeMiddleware } from './mergeMiddleware'; +import type { ContextConstraint, MiddlewareContext } from './v2'; import { deepClone, fromLegacyRequest, @@ -47,12 +48,13 @@ export function asV2Middleware< * @returns The {@link JsonRpcEngineV2} middleware. */ export function asV2Middleware< - Params extends JsonRpcParams, - Request extends JsonRpcRequest, - Result extends Json, + Params extends JsonRpcParams = JsonRpcParams, + Request extends JsonRpcRequest = JsonRpcRequest, + Result extends ResultConstraint = ResultConstraint, + Context extends ContextConstraint = MiddlewareContext, >( ...middleware: LegacyMiddleware[] -): JsonRpcMiddleware; +): JsonRpcMiddleware; /** * The asV2Middleware implementation. diff --git a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts index c9431b84959..43eabd5c51b 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts @@ -44,7 +44,7 @@ export type Next = ( export type MiddlewareParams< Request extends JsonRpcCall = JsonRpcCall, - Context extends MiddlewareContext = MiddlewareContext, + Context extends ContextConstraint = MiddlewareContext, > = { request: Readonly; context: Context; diff --git a/packages/json-rpc-engine/src/v2/createScaffoldMiddleware.ts b/packages/json-rpc-engine/src/v2/createScaffoldMiddleware.ts index 88c7212db9f..6eaf1bdc450 100644 --- a/packages/json-rpc-engine/src/v2/createScaffoldMiddleware.ts +++ b/packages/json-rpc-engine/src/v2/createScaffoldMiddleware.ts @@ -1,23 +1,32 @@ import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import type { JsonRpcMiddleware } from './JsonRpcEngineV2'; +import type { ContextConstraint, MiddlewareContext } from './MiddlewareContext'; // Only permit primitive values as hard-coded scaffold middleware results. type JsonPrimitive = string | number | boolean | null; +/** + * A handler for a scaffold middleware function. + * + * @template Params - The parameters of the request. + * @template Result - The result of the request. + * @template Context - The context of the request. + * @returns A JSON-RPC middleware function or a primitive JSON value. + */ export type ScaffoldMiddlewareHandler< Params extends JsonRpcParams, Result extends Json, -> = JsonRpcMiddleware, Result> | JsonPrimitive; + Context extends ContextConstraint, +> = JsonRpcMiddleware, Result, Context> | JsonPrimitive; /** * A record of RPC method handler functions or hard-coded results, keyed to particular method names. * Only primitive JSON values are permitted as hard-coded results. */ -export type MiddlewareScaffold = Record< - string, - ScaffoldMiddlewareHandler ->; +export type MiddlewareScaffold< + Context extends ContextConstraint = MiddlewareContext, +> = Record>; /** * Creates a middleware function from an object of RPC method handler functions, @@ -28,9 +37,9 @@ export type MiddlewareScaffold = Record< * @param handlers - The RPC method handler functions. * @returns The scaffold middleware function. */ -export function createScaffoldMiddleware( - handlers: MiddlewareScaffold, -): JsonRpcMiddleware { +export function createScaffoldMiddleware( + handlers: MiddlewareScaffold, +): JsonRpcMiddleware { return ({ request, context, next }) => { const handlerOrResult = handlers[request.method]; if (handlerOrResult === undefined) { diff --git a/packages/message-manager/CHANGELOG.md b/packages/message-manager/CHANGELOG.md index fe2ff35baee..ece5141f53f 100644 --- a/packages/message-manager/CHANGELOG.md +++ b/packages/message-manager/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Rename `OriginalRequest` type to `MessageRequest` and permit string `id` values ([#7065](https://github.com/MetaMask/core/pull/7065)) + - Previously, only number values were permitted for the `id` property. + ## [14.0.0] ### Changed diff --git a/packages/message-manager/src/AbstractMessageManager.test.ts b/packages/message-manager/src/AbstractMessageManager.test.ts index ee696f53bd8..36d178bcb65 100644 --- a/packages/message-manager/src/AbstractMessageManager.test.ts +++ b/packages/message-manager/src/AbstractMessageManager.test.ts @@ -11,7 +11,7 @@ import { type AbstractMessage, type AbstractMessageParams, type MessageManagerState, - type OriginalRequest, + type MessageRequest, type SecurityProviderRequest, } from './AbstractMessageManager'; @@ -50,7 +50,7 @@ class AbstractTestManager extends AbstractMessageManager< > { addRequestToMessageParams( messageParams: MessageParams, - req?: OriginalRequest, + req?: MessageRequest, ) { return super.addRequestToMessageParams(messageParams, req); } @@ -58,7 +58,7 @@ class AbstractTestManager extends AbstractMessageManager< createUnapprovedMessage( messageParams: MessageParams, type: ApprovalType, - req?: OriginalRequest, + req?: MessageRequest, ) { return super.createUnapprovedMessage(messageParams, type, req); } diff --git a/packages/message-manager/src/AbstractMessageManager.ts b/packages/message-manager/src/AbstractMessageManager.ts index 14054ed42a7..86fff7f5ba8 100644 --- a/packages/message-manager/src/AbstractMessageManager.ts +++ b/packages/message-manager/src/AbstractMessageManager.ts @@ -37,14 +37,10 @@ const getDefaultState = () => ({ }); /** - * @type OriginalRequest - * - * Represents the original request object for adding a message. - * - * @property origin? - Is it is specified, represents the origin + * Represents the request adding a message. */ -export type OriginalRequest = { - id?: number; +export type MessageRequest = { + id?: string | number; origin?: string; securityAlertResponse?: Record; }; @@ -85,7 +81,7 @@ export type AbstractMessage = { export type AbstractMessageParams = { from: string; origin?: string; - requestId?: number; + requestId?: string | number; deferSetAsSigned?: boolean; }; @@ -216,7 +212,7 @@ export abstract class AbstractMessageManager< */ protected addRequestToMessageParams< MessageParams extends AbstractMessageParams, - >(messageParams: MessageParams, req?: OriginalRequest) { + >(messageParams: MessageParams, req?: MessageRequest) { const updatedMessageParams = { ...messageParams, }; @@ -239,7 +235,7 @@ export abstract class AbstractMessageManager< */ protected createUnapprovedMessage< MessageParams extends AbstractMessageParams, - >(messageParams: MessageParams, type: ApprovalType, req?: OriginalRequest) { + >(messageParams: MessageParams, type: ApprovalType, req?: MessageRequest) { const messageId = random(); return { @@ -525,7 +521,7 @@ export abstract class AbstractMessageManager< */ abstract addUnapprovedMessage( messageParams: ParamsMetamask, - request: OriginalRequest, + request: MessageRequest, version?: string, ): Promise; diff --git a/packages/message-manager/src/DecryptMessageManager.ts b/packages/message-manager/src/DecryptMessageManager.ts index 81f6c66934e..cb014bb0e4a 100644 --- a/packages/message-manager/src/DecryptMessageManager.ts +++ b/packages/message-manager/src/DecryptMessageManager.ts @@ -11,7 +11,7 @@ import type { AbstractMessageParams, AbstractMessageParamsMetamask, MessageManagerState, - OriginalRequest, + MessageRequest, SecurityProviderRequest, } from './AbstractMessageManager'; import { AbstractMessageManager } from './AbstractMessageManager'; @@ -133,7 +133,7 @@ export class DecryptMessageManager extends AbstractMessageManager< */ async addUnapprovedMessageAsync( messageParams: DecryptMessageParams, - req?: OriginalRequest, + req?: MessageRequest, ): Promise { validateDecryptedMessageData(messageParams); const messageId = await this.addUnapprovedMessage(messageParams, req); @@ -183,7 +183,7 @@ export class DecryptMessageManager extends AbstractMessageManager< */ async addUnapprovedMessage( messageParams: DecryptMessageParams, - req?: OriginalRequest, + req?: MessageRequest, ) { const updatedMessageParams = this.addRequestToMessageParams( messageParams, diff --git a/packages/message-manager/src/EncryptionPublicKeyManager.ts b/packages/message-manager/src/EncryptionPublicKeyManager.ts index 9b3fec821dc..371747725f2 100644 --- a/packages/message-manager/src/EncryptionPublicKeyManager.ts +++ b/packages/message-manager/src/EncryptionPublicKeyManager.ts @@ -10,7 +10,7 @@ import type { AbstractMessageParams, AbstractMessageParamsMetamask, MessageManagerState, - OriginalRequest, + MessageRequest, SecurityProviderRequest, } from './AbstractMessageManager'; import { AbstractMessageManager } from './AbstractMessageManager'; @@ -133,7 +133,7 @@ export class EncryptionPublicKeyManager extends AbstractMessageManager< */ async addUnapprovedMessageAsync( messageParams: EncryptionPublicKeyParams, - req?: OriginalRequest, + req?: MessageRequest, ): Promise { validateEncryptionPublicKeyMessageData(messageParams); const messageId = await this.addUnapprovedMessage(messageParams, req); @@ -177,7 +177,7 @@ export class EncryptionPublicKeyManager extends AbstractMessageManager< */ async addUnapprovedMessage( messageParams: EncryptionPublicKeyParams, - req?: OriginalRequest, + req?: MessageRequest, ): Promise { const updatedMessageParams = this.addRequestToMessageParams( messageParams, diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 518ae057ea3..754630d8fde 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The network client middleware, via `@metamask/eth-json-rpc-middleware`, will now throw an error if it encounters an `undefined` result when dispatching a request with a later block number than the originally requested block number. - In practice, this should happen rarely if ever. +- **BREAKING:** Migrate `NetworkClient` to `JsonRpcEngineV2` ([#7065](https://github.com/MetaMask/core/pull/7065)) + - This ought to be unobservable, but we mark it as breaking out of an abundance of caution. - Bump `@metamask/controller-utils` from `^11.14.1` to `^11.15.0` ([#7003](https://github.com/MetaMask/core/pull/7003)) ### Fixed diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index ece0f425aaa..a0672a0c503 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -73,10 +73,12 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/error-reporting-service": "^3.0.0", "@ts-bridge/cli": "^0.6.4", + "@types/deep-freeze-strict": "^1.1.0", "@types/jest": "^27.4.1", "@types/jest-when": "^2.7.3", "@types/lodash": "^4.14.191", "@types/node-fetch": "^2.6.12", + "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", "jest-when": "^3.4.2", diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 5863aa1de35..2bb04f387ad 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -2444,6 +2444,8 @@ export class NetworkController extends BaseController< * * In-progress requests will not be aborted. */ + // We're intentionally changing the signature of an extended method. + // eslint-disable-next-line @typescript-eslint/no-misused-promises async destroy() { await this.#blockTrackerProxy?.destroy(); } diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index bc95fdd38eb..8ae4565b767 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -12,18 +12,18 @@ import { createFetchMiddleware, createRetryOnEmptyMiddleware, } from '@metamask/eth-json-rpc-middleware'; +import { InternalProvider } from '@metamask/eth-json-rpc-provider'; +import { providerFromMiddlewareV2 } from '@metamask/eth-json-rpc-provider'; +import { asV2Middleware } from '@metamask/json-rpc-engine'; import { - InternalProvider, - providerFromMiddleware, -} from '@metamask/eth-json-rpc-provider'; -import { - createAsyncMiddleware, createScaffoldMiddleware, - JsonRpcEngine, - mergeMiddleware, -} from '@metamask/json-rpc-engine'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import type { Hex, Json, JsonRpcParams } from '@metamask/utils'; + JsonRpcEngineV2, +} from '@metamask/json-rpc-engine/v2'; +import type { + JsonRpcMiddleware, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; +import type { Hex, Json, JsonRpcRequest } from '@metamask/utils'; import type { Logger } from 'loglevel'; import type { NetworkControllerMessenger } from './NetworkController'; @@ -49,6 +49,12 @@ export type NetworkClient = { destroy: () => void; }; +type RpcApiMiddleware = JsonRpcMiddleware< + JsonRpcRequest, + Json, + MiddlewareContext<{ origin: string }> +>; + /** * Create a JSON RPC network client for a specific network. * @@ -136,17 +142,21 @@ export function createNetworkClient({ }); }); - const rpcApiMiddleware = - configuration.type === NetworkClientType.Infura - ? createInfuraMiddleware({ - rpcService: rpcServiceChain, - options: { - source: 'metamask', - }, - }) - : createFetchMiddleware({ rpcService: rpcServiceChain }); + let rpcApiMiddleware: RpcApiMiddleware; + if (configuration.type === NetworkClientType.Infura) { + rpcApiMiddleware = asV2Middleware( + createInfuraMiddleware({ + rpcService: rpcServiceChain, + options: { + source: 'metamask', + }, + }), + ); + } else { + rpcApiMiddleware = createFetchMiddleware({ rpcService: rpcServiceChain }); + } - const rpcProvider = providerFromMiddleware(rpcApiMiddleware); + const rpcProvider = providerFromMiddlewareV2(rpcApiMiddleware); const blockTracker = createBlockTracker({ networkClientType: configuration.type, @@ -169,11 +179,11 @@ export function createNetworkClient({ rpcApiMiddleware, }); - const engine = new JsonRpcEngine(); - - engine.push(networkMiddleware); - - const provider: Provider = new InternalProvider({ engine }); + const provider: Provider = new InternalProvider({ + engine: JsonRpcEngineV2.create({ + middleware: [networkMiddleware], + }), + }); const destroy = () => { // TODO: Either fix this lint violation or explain why it's necessary to ignore. @@ -240,17 +250,19 @@ function createInfuraNetworkMiddleware({ blockTracker: PollingBlockTracker; network: InfuraNetworkType; rpcProvider: InternalProvider; - rpcApiMiddleware: JsonRpcMiddleware; + rpcApiMiddleware: RpcApiMiddleware; }) { - return mergeMiddleware([ - createNetworkAndChainIdMiddleware({ network }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), - createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), - createBlockTrackerInspectorMiddleware({ blockTracker }), - rpcApiMiddleware, - ]); + return JsonRpcEngineV2.create({ + middleware: [ + createNetworkAndChainIdMiddleware({ network }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), + createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ], + }).asMiddleware(); } /** @@ -272,11 +284,10 @@ function createNetworkAndChainIdMiddleware({ const createChainIdMiddleware = ( chainId: Hex, -): JsonRpcMiddleware => { - return (req, res, next, end) => { - if (req.method === 'eth_chainId') { - res.result = chainId; - return end(); +): JsonRpcMiddleware => { + return ({ request, next }) => { + if (request.method === 'eth_chainId') { + return chainId; } return next(); }; @@ -298,21 +309,23 @@ function createCustomNetworkMiddleware({ }: { blockTracker: PollingBlockTracker; chainId: Hex; - rpcApiMiddleware: JsonRpcMiddleware; -}): JsonRpcMiddleware { + rpcApiMiddleware: RpcApiMiddleware; +}) { const testMiddlewares = process.env.IN_TEST ? [createEstimateGasDelayTestMiddleware()] : []; - return mergeMiddleware([ - ...testMiddlewares, - createChainIdMiddleware(chainId), - createBlockRefRewriteMiddleware({ blockTracker }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockTrackerInspectorMiddleware({ blockTracker }), - rpcApiMiddleware, - ]); + return JsonRpcEngineV2.create({ + middleware: [ + ...testMiddlewares, + createChainIdMiddleware(chainId), + createBlockRefRewriteMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ], + }).asMiddleware(); } /** @@ -321,11 +334,14 @@ function createCustomNetworkMiddleware({ * * @returns The middleware for delaying gas estimation calls by 2 seconds when in test. */ -function createEstimateGasDelayTestMiddleware() { - return createAsyncMiddleware(async (req, _, next) => { - if (req.method === 'eth_estimateGas') { +function createEstimateGasDelayTestMiddleware(): JsonRpcMiddleware< + JsonRpcRequest, + Json +> { + return async ({ request, next }) => { + if (request.method === 'eth_estimateGas') { await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); } return next(); - }); + }; } diff --git a/packages/network-controller/src/rpc-service/rpc-service.test.ts b/packages/network-controller/src/rpc-service/rpc-service.test.ts index 3bd4249d3b7..83e2aeaacec 100644 --- a/packages/network-controller/src/rpc-service/rpc-service.test.ts +++ b/packages/network-controller/src/rpc-service/rpc-service.test.ts @@ -3,6 +3,7 @@ import { HttpError } from '@metamask/controller-utils'; import { errorCodes } from '@metamask/rpc-errors'; +import deepFreeze from 'deep-freeze-strict'; import nock from 'nock'; import { FetchError } from 'node-fetch'; import { useFakeTimers } from 'sinon'; @@ -934,6 +935,42 @@ describe('RpcService', () => { }); }); + it('handles deeply frozen JSON-RPC requests', async () => { + const endpointUrl = 'https://rpc.example.chain'; + nock(endpointUrl) + .post('/', { + id: 1, + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + }) + .reply(200, { + id: 1, + jsonrpc: '2.0', + result: '0x1', + }); + const service = new RpcService({ + fetch, + btoa, + endpointUrl, + }); + + const response = await service.request( + deepFreeze({ + id: 1, + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + }), + ); + + expect(response).toStrictEqual({ + id: 1, + jsonrpc: '2.0', + result: '0x1', + }); + }); + it('does not throw if the endpoint returns an unsuccessful JSON-RPC response', async () => { const endpointUrl = 'https://rpc.example.chain'; nock(endpointUrl) diff --git a/packages/network-controller/src/rpc-service/rpc-service.ts b/packages/network-controller/src/rpc-service/rpc-service.ts index 51f5876fed4..7021e8167cd 100644 --- a/packages/network-controller/src/rpc-service/rpc-service.ts +++ b/packages/network-controller/src/rpc-service/rpc-service.ts @@ -429,7 +429,8 @@ export class RpcService implements AbstractRpcService { ): Promise>; async request( - jsonRpcRequest: JsonRpcRequest, + // The request object may be frozen and must not be mutated. + jsonRpcRequest: Readonly>, fetchOptions: FetchOptions = {}, ): Promise> { const completeFetchOptions = this.#getCompleteFetchOptions( @@ -489,7 +490,7 @@ export class RpcService implements AbstractRpcService { * @returns The complete set of `fetch` options. */ #getCompleteFetchOptions( - jsonRpcRequest: JsonRpcRequest, + jsonRpcRequest: Readonly>, fetchOptions: FetchOptions, ): FetchOptions { const defaultOptions = { diff --git a/packages/signature-controller/CHANGELOG.md b/packages/signature-controller/CHANGELOG.md index 9992233752b..8945e753dd5 100644 --- a/packages/signature-controller/CHANGELOG.md +++ b/packages/signature-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Permit string `id` values in `OriginalRequest` type ([#7065](https://github.com/MetaMask/core/pull/7065)) + - Previously, only number values were permitted for the `id` property. + ## [36.0.0] ### Changed diff --git a/packages/signature-controller/src/types.ts b/packages/signature-controller/src/types.ts index cbfa4ca5ba4..a7346b660d2 100644 --- a/packages/signature-controller/src/types.ts +++ b/packages/signature-controller/src/types.ts @@ -27,7 +27,7 @@ export enum DecodingDataChangeType { /** Original client request that triggered the signature request. */ export type OriginalRequest = { /** Unique ID to identify the client request. */ - id?: number; + id?: number | string; /** Method of signature request */ method?: string; diff --git a/yarn.lock b/yarn.lock index 940faf5489a..9f7926f4abd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3467,13 +3467,16 @@ __metadata: "@metamask/eth-json-rpc-provider": "npm:^5.0.1" "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/json-rpc-engine": "npm:^10.1.1" + "@metamask/message-manager": "npm:^14.0.0" "@metamask/network-controller": "npm:^25.0.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.8.1" "@ts-bridge/cli": "npm:^0.6.4" + "@types/deep-freeze-strict": "npm:^1.1.0" "@types/jest": "npm:^27.4.1" "@types/pify": "npm:^5.0.2" + deep-freeze-strict: "npm:^1.1.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" klona: "npm:^2.0.6" @@ -4008,7 +4011,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/message-manager@workspace:packages/message-manager": +"@metamask/message-manager@npm:^14.0.0, @metamask/message-manager@workspace:packages/message-manager": version: 0.0.0-use.local resolution: "@metamask/message-manager@workspace:packages/message-manager" dependencies: @@ -4245,11 +4248,13 @@ __metadata: "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^11.8.1" "@ts-bridge/cli": "npm:^0.6.4" + "@types/deep-freeze-strict": "npm:^1.1.0" "@types/jest": "npm:^27.4.1" "@types/jest-when": "npm:^2.7.3" "@types/lodash": "npm:^4.14.191" "@types/node-fetch": "npm:^2.6.12" async-mutex: "npm:^0.5.0" + deep-freeze-strict: "npm:^1.1.1" deepmerge: "npm:^4.2.2" fast-deep-equal: "npm:^3.1.3" immer: "npm:^9.0.6"