diff --git a/package.json b/package.json index 9d08a714ff3..7d237a4c205 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@metamask/eth-block-tracker": "^14.0.0", "@metamask/eth-json-rpc-provider": "^5.0.1", "@metamask/json-rpc-engine": "^10.1.1", + "@metamask/network-controller": "^25.0.0", "@metamask/utils": "^11.8.1", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^27.4.1", diff --git a/packages/bridge-controller/src/utils/balance.test.ts b/packages/bridge-controller/src/utils/balance.test.ts index 88c3654ea02..ede03d0b49b 100644 --- a/packages/bridge-controller/src/utils/balance.test.ts +++ b/packages/bridge-controller/src/utils/balance.test.ts @@ -2,8 +2,8 @@ import { BigNumber } from '@ethersproject/bignumber'; import { AddressZero } from '@ethersproject/constants'; import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; -import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { abiERC20 } from '@metamask/metamask-eth-abis'; +import type { Provider } from '@metamask/network-controller'; import * as balanceUtils from './balance'; import { fetchTokenBalance } from './balance'; @@ -11,7 +11,7 @@ import { FakeProvider } from '../../../../tests/fake-provider'; declare global { // eslint-disable-next-line no-var - var ethereumProvider: InternalProvider; + var ethereumProvider: Provider; } jest.mock('@ethersproject/contracts', () => { diff --git a/packages/eth-block-tracker/CHANGELOG.md b/packages/eth-block-tracker/CHANGELOG.md index d7d7073da4a..cc75aa2ca3c 100644 --- a/packages/eth-block-tracker/CHANGELOG.md +++ b/packages/eth-block-tracker/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `Context` generic parameter to `PollingBlockTracker` ([#7061](https://github.com/MetaMask/core/pull/7061)) + - This enables passing providers with different context types to the block tracker. + ### Changed - **BREAKING:** Use `InternalProvider` instead of `SafeEventEmitterProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) diff --git a/packages/eth-block-tracker/src/PollingBlockTracker.ts b/packages/eth-block-tracker/src/PollingBlockTracker.ts index c825c36363f..1ba5a9f8275 100644 --- a/packages/eth-block-tracker/src/PollingBlockTracker.ts +++ b/packages/eth-block-tracker/src/PollingBlockTracker.ts @@ -1,4 +1,8 @@ import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; +import type { + ContextConstraint, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; import SafeEventEmitter from '@metamask/safe-event-emitter'; import { createDeferredPromise, @@ -17,8 +21,10 @@ const sec = 1000; const blockTrackerEvents: (string | symbol)[] = ['sync', 'latest']; -export type PollingBlockTrackerOptions = { - provider?: InternalProvider; +export type PollingBlockTrackerOptions< + Context extends ContextConstraint = MiddlewareContext, +> = { + provider?: InternalProvider; pollingInterval?: number; retryTimeout?: number; keepEventLoopActive?: boolean; @@ -33,7 +39,9 @@ type ExtendedJsonRpcRequest = { type InternalListener = (value: string) => void; -export class PollingBlockTracker +export class PollingBlockTracker< + Context extends ContextConstraint = MiddlewareContext, + > extends SafeEventEmitter implements BlockTracker { @@ -49,7 +57,7 @@ export class PollingBlockTracker private _pollingTimeout?: ReturnType; - private readonly _provider: InternalProvider; + private readonly _provider: InternalProvider; private readonly _pollingInterval: number; @@ -65,7 +73,7 @@ export class PollingBlockTracker #pendingFetch?: Omit, 'resolve'>; - constructor(opts: PollingBlockTrackerOptions = {}) { + constructor(opts: PollingBlockTrackerOptions = {}) { // parse + validate args if (!opts.provider) { throw new Error('PollingBlockTracker - no provider specified.'); diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index b564b68d55e..6ae2294ac84 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `providerFromMiddlewareV2` ([#7001](https://github.com/MetaMask/core/pull/7001)) - This accepts the new middleware from `@metamask/json-rpc-engine/v2`. +- Add `context` option to `InternalProvider.request()` ([#7061](https://github.com/MetaMask/core/pull/7061)) + - Enables passing a `MiddlewareContext` to the JSON-RPC server. ### Changed diff --git a/packages/eth-json-rpc-provider/src/internal-provider.test.ts b/packages/eth-json-rpc-provider/src/internal-provider.test.ts index 6e102647d9e..322958d9dc3 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.test.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.test.ts @@ -2,7 +2,10 @@ import { Web3Provider } from '@ethersproject/providers'; import EthQuery from '@metamask/eth-query'; import EthJsQuery from '@metamask/ethjs-query'; import { asV2Middleware, JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import type { + JsonRpcMiddleware, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { type JsonRpcRequest, type Json } from '@metamask/utils'; @@ -16,7 +19,9 @@ import { jest.mock('uuid'); -type ResultParam = Json | ((req?: JsonRpcRequest) => Json); +type ResultParam = + | Json + | ((req?: JsonRpcRequest, context?: MiddlewareContext) => Json); const createLegacyEngine = (method: string, result: ResultParam) => { const engine = new JsonRpcEngine(); @@ -33,9 +38,11 @@ const createLegacyEngine = (method: string, result: ResultParam) => { const createV2Engine = (method: string, result: ResultParam) => { return JsonRpcEngineV2.create>({ middleware: [ - ({ request, next }) => { + ({ request, next, context }) => { if (request.method === method) { - return typeof result === 'function' ? result(request) : result; + return typeof result === 'function' + ? result(request as JsonRpcRequest, context) + : result; } return next(); }, @@ -245,6 +252,29 @@ describe.each([ expect(response.result).toBe(42); }); + it('forwards the context to the JSON-RPC handler', async () => { + const rpcHandler = createRpcHandler('test', (request, context) => { + // @ts-expect-error - Intentional type abuse. + // eslint-disable-next-line jest/no-conditional-in-test + return context?.assertGet('foo') ?? request.foo; + }); + const provider = new InternalProvider({ engine: rpcHandler }); + + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: 'test', + }; + + const result = await provider.request(request, { + context: { + foo: 'bar', + }, + }); + + expect(result).toBe('bar'); + }); + it('handles a successful EIP-1193 object request', async () => { let req: JsonRpcRequest | undefined; const rpcHandler = createRpcHandler('test', (request) => { diff --git a/packages/eth-json-rpc-provider/src/internal-provider.ts b/packages/eth-json-rpc-provider/src/internal-provider.ts index 920416c7317..fe8daee042c 100644 --- a/packages/eth-json-rpc-provider/src/internal-provider.ts +++ b/packages/eth-json-rpc-provider/src/internal-provider.ts @@ -1,9 +1,10 @@ import { asV2Middleware, type JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { - ContextConstraint, - MiddlewareContext, +import { + type HandleOptions, + type ContextConstraint, + type MiddlewareContext, + JsonRpcEngineV2, } from '@metamask/json-rpc-engine/v2'; -import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import type { JsonRpcSuccess } from '@metamask/utils'; import { type Json, @@ -37,8 +38,10 @@ type Options< * This provider loosely follows conventions that pre-date EIP-1193. * It is not compliant with any Ethereum provider standard. */ -export class InternalProvider { - readonly #engine: JsonRpcEngineV2; +export class InternalProvider< + Context extends ContextConstraint = MiddlewareContext, +> { + readonly #engine: JsonRpcEngineV2; /** * Construct a InternalProvider from a JSON-RPC server or legacy engine. @@ -46,7 +49,7 @@ export class InternalProvider { * @param options - Options. * @param options.engine - The JSON-RPC engine used to process requests. */ - constructor({ engine }: Options) { + constructor({ engine }: Options) { this.#engine = 'push' in engine ? JsonRpcEngineV2.create({ @@ -59,14 +62,17 @@ export class InternalProvider { * Send a provider request asynchronously. * * @param eip1193Request - The request to send. + * @param options - The options for the request operation. + * @param options.context - The context to include with the request. * @returns The JSON-RPC response. */ async request( eip1193Request: Eip1193Request, + options?: HandleOptions, ): Promise { const jsonRpcRequest = convertEip1193RequestToJsonRpcRequest(eip1193Request); - return (await this.#handle(jsonRpcRequest)).result; + return (await this.#handle(jsonRpcRequest, options)).result; } /** @@ -118,12 +124,14 @@ export class InternalProvider { readonly #handle = async ( jsonRpcRequest: JsonRpcRequest, + options?: HandleOptions, ): Promise> => { const { id, jsonrpc } = jsonRpcRequest; - // This typecast is technicaly unsafe, but we need it to preserve the provider's - // public interface, which allows you to typecast results. + // The `result` typecast is unsafe, but we need it to preserve the provider's + // public interface, which allows you to unsafely typecast results. const result = (await this.#engine.handle( jsonRpcRequest, + options, )) as unknown as Result; return { diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index 2f2d9c57f0a..ea26e0a1b9d 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), [#7001](https://github.com/MetaMask/core/pull/7001), [#7032](https://github.com/MetaMask/core/pull/7032)) +- Add `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7032](https://github.com/MetaMask/core/pull/7032), [#7001](https://github.com/MetaMask/core/pull/7001), [#7061](https://github.com/MetaMask/core/pull/7061)) - 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 8611bba9141..ff10d2dc7f3 100644 --- a/packages/json-rpc-engine/README.md +++ b/packages/json-rpc-engine/README.md @@ -444,6 +444,33 @@ const engine = JsonRpcEngineV2.create({ }); ``` +#### Passing the context to `handle()` + +You can pass a `MiddlewareContext` instance directly to `handle()`: + +```ts +const context = new MiddlewareContext(); +context.set('foo', 'bar'); +const result = await engine.handle( + { id: '1', jsonrpc: '2.0', method: 'hello' }, + { context }, +); +console.log(result); // 'bar' +``` + +You can also pass a plain object as a shorthand for a `MiddlewareContext` instance: + +```ts +const context = { foo: 'bar' }; +const result = await engine.handle( + { id: '1', jsonrpc: '2.0', method: 'hello' }, + { context }, +); +console.log(result); // 'bar' +``` + +This works the same way for `JsonRpcServer.handle()`. + #### Constraining context keys and values The context exposes a generic parameter `KeyValues`, which determines the keys and values diff --git a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.test.ts b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.test.ts index ad5cbaddc0c..a3d2eb91094 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.test.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.test.ts @@ -371,6 +371,24 @@ describe('JsonRpcEngineV2', () => { expect(result).toBe('bar'); }); + it('accepts an initial context as a KeyValues object', async () => { + const initialContext = { foo: 'bar' } as const; + const middleware: JsonRpcMiddleware< + JsonRpcRequest, + string, + MiddlewareContext> + > = ({ context }) => context.assertGet('foo'); + const engine = JsonRpcEngineV2.create({ + middleware: [middleware], + }); + + const result = await engine.handle(makeRequest(), { + context: initialContext, + }); + + expect(result).toBe('bar'); + }); + it('accepts middleware with different context types', async () => { const middleware1: JsonRpcMiddleware< JsonRpcCall, diff --git a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts index a6c4dce794a..c9431b84959 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts @@ -7,7 +7,11 @@ import { } from '@metamask/utils'; import deepFreeze from 'deep-freeze-strict'; -import type { ContextConstraint, MergeContexts } from './MiddlewareContext'; +import type { + ContextConstraint, + InferKeyValues, + MergeContexts, +} from './MiddlewareContext'; import { MiddlewareContext } from './MiddlewareContext'; import { isNotification, @@ -60,8 +64,11 @@ type RequestState = { result: Readonly> | undefined; }; -type HandleOptions = { - context?: Context; +/** + * The options for the JSON-RPC request/notification handling operation. + */ +export type HandleOptions = { + context?: Context | InferKeyValues; }; type ConstructorOptions< @@ -286,12 +293,14 @@ export class JsonRpcEngineV2< * operation. Permits returning an `undefined` result. * * @param originalRequest - The JSON-RPC request to handle. - * @param context - The context to pass to the middleware. + * @param rawContext - The context to pass to the middleware. * @returns The result from the middleware. */ async #handle( originalRequest: Request, - context: Context = new MiddlewareContext() as Context, + rawContext: + | Context + | InferKeyValues = new MiddlewareContext() as Context, ): Promise> { this.#assertIsNotDestroyed(); @@ -303,6 +312,9 @@ export class JsonRpcEngineV2< }; const middlewareIterator = this.#makeMiddlewareIterator(); const firstMiddleware = middlewareIterator.next().value; + const context = MiddlewareContext.isInstance(rawContext) + ? rawContext + : (new MiddlewareContext(rawContext) as Context); const makeNext = this.#makeNextFactory(middlewareIterator, state, context); diff --git a/packages/json-rpc-engine/src/v2/JsonRpcServer.test.ts b/packages/json-rpc-engine/src/v2/JsonRpcServer.test.ts index df6a6ce3e2d..54c3e337d12 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcServer.test.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcServer.test.ts @@ -3,6 +3,7 @@ import { rpcErrors } from '@metamask/rpc-errors'; import type { JsonRpcMiddleware } from './JsonRpcEngineV2'; import { JsonRpcEngineV2 } from './JsonRpcEngineV2'; import { JsonRpcServer } from './JsonRpcServer'; +import type { MiddlewareContext } from './MiddlewareContext'; import type { JsonRpcNotification, JsonRpcRequest } from './utils'; import { isRequest, JsonRpcEngineError, stringify } from './utils'; @@ -108,6 +109,39 @@ describe('JsonRpcServer', () => { expect(response).toBeUndefined(); }); + it('forwards the context to the engine', async () => { + const middleware: JsonRpcMiddleware< + JsonRpcRequest, + string, + MiddlewareContext<{ foo: string }> + > = ({ context }) => { + return context.assertGet('foo'); + }; + const server = new JsonRpcServer({ + middleware: [middleware], + onError: () => undefined, + }); + + const response = await server.handle( + { + jsonrpc, + id: 1, + method: 'hello', + }, + { + context: { + foo: 'bar', + }, + }, + ); + + expect(response).toStrictEqual({ + jsonrpc, + id: 1, + result: 'bar', + }); + }); + it('returns an error response for a failed request', async () => { const server = new JsonRpcServer({ engine: makeEngine(), diff --git a/packages/json-rpc-engine/src/v2/JsonRpcServer.ts b/packages/json-rpc-engine/src/v2/JsonRpcServer.ts index dbf782aec64..068b721c52c 100644 --- a/packages/json-rpc-engine/src/v2/JsonRpcServer.ts +++ b/packages/json-rpc-engine/src/v2/JsonRpcServer.ts @@ -9,6 +9,7 @@ import type { import { hasProperty, isObject } from '@metamask/utils'; import type { + HandleOptions, JsonRpcMiddleware, MergedContextOf, MiddlewareConstraint, @@ -102,9 +103,14 @@ export class JsonRpcServer< * engine. The request will fail if the engine can only handle notifications. * * @param request - The request to handle. + * @param options - The options for the handle operation. + * @param options.context - The context to pass to the middleware. * @returns The JSON-RPC response. */ - async handle(request: JsonRpcRequest): Promise; + async handle( + request: JsonRpcRequest, + options?: HandleOptions>, + ): Promise; /** * Handle a JSON-RPC notification. @@ -116,8 +122,13 @@ export class JsonRpcServer< * engine. The request will fail if the engine cannot handle notifications. * * @param notification - The notification to handle. + * @param options - The options for the handle operation. + * @param options.context - The context to pass to the middleware. */ - async handle(notification: JsonRpcNotification): Promise; + async handle( + notification: JsonRpcNotification, + options?: HandleOptions>, + ): Promise; /** * Handle an alleged JSON-RPC request or notification. Permits any plain @@ -133,22 +144,30 @@ export class JsonRpcServer< * response) is not of the type expected by the underlying engine. * * @param rawRequest - The raw request to handle. + * @param options - The options for the handle operation. + * @param options.context - The context to pass to the middleware. * @returns The JSON-RPC response, or `undefined` if the request is a * notification. */ - async handle(rawRequest: unknown): Promise; + async handle( + rawRequest: unknown, + options?: HandleOptions>, + ): Promise; - async handle(rawRequest: unknown): Promise { + async handle( + rawRequest: unknown, + options?: HandleOptions>, + ): Promise { // If rawRequest is not a notification, the originalId will be attached // to the response. We attach our own, trusted id in #coerceRequest() // while the request is being handled. const [originalId, isRequest] = getOriginalId(rawRequest); try { - const request = this.#coerceRequest(rawRequest, isRequest); + const request = JsonRpcServer.#coerceRequest(rawRequest, isRequest); // @ts-expect-error - The request may not be of the type expected by the engine, // and we intentionally allow this to happen. - const result = await this.#engine.handle(request); + const result = await this.#engine.handle(request, options); if (result !== undefined) { return { @@ -176,7 +195,7 @@ export class JsonRpcServer< return undefined; } - #coerceRequest(rawRequest: unknown, isRequest: boolean): JsonRpcCall { + static #coerceRequest(rawRequest: unknown, isRequest: boolean): JsonRpcCall { if (!isMinimalRequest(rawRequest)) { throw rpcErrors.invalidRequest({ data: { diff --git a/packages/json-rpc-engine/src/v2/MiddlewareContext.test.ts b/packages/json-rpc-engine/src/v2/MiddlewareContext.test.ts index 8d755ddb485..b88f8421c3d 100644 --- a/packages/json-rpc-engine/src/v2/MiddlewareContext.test.ts +++ b/packages/json-rpc-engine/src/v2/MiddlewareContext.test.ts @@ -11,6 +11,17 @@ describe('MiddlewareContext', () => { expect(context.get(symbol)).toBe('value'); }); + it('can be constructed with a KeyValues object', () => { + const symbol = Symbol('symbol'); + const context = new MiddlewareContext<{ test: string; [symbol]: string }>({ + test: 'string value', + [symbol]: 'symbol value', + }); + + expect(context.get('test')).toBe('string value'); + expect(context.get(symbol)).toBe('symbol value'); + }); + it('is frozen', () => { const context = new MiddlewareContext(); expect(Object.isFrozen(context)).toBe(true); @@ -63,4 +74,11 @@ describe('MiddlewareContext', () => { `MiddlewareContext key "test" already exists`, ); }); + + it('identifies instances of MiddlewareContext via isInstance', () => { + const context = new MiddlewareContext(); + + expect(MiddlewareContext.isInstance(context)).toBe(true); + expect(MiddlewareContext.isInstance({ foo: 'bar' })).toBe(false); + }); }); diff --git a/packages/json-rpc-engine/src/v2/MiddlewareContext.ts b/packages/json-rpc-engine/src/v2/MiddlewareContext.ts index 073b7e0c760..75d64203868 100644 --- a/packages/json-rpc-engine/src/v2/MiddlewareContext.ts +++ b/packages/json-rpc-engine/src/v2/MiddlewareContext.ts @@ -1,4 +1,6 @@ -import type { UnionToIntersection } from './utils'; +import { isInstance, type UnionToIntersection } from './utils'; + +const MiddlewareContextSymbol = Symbol.for('json-rpc-engine#MiddlewareContext'); /** * An context object for middleware that attempts to protect against accidental @@ -27,10 +29,29 @@ import type { UnionToIntersection } from './utils'; export class MiddlewareContext< KeyValues extends Record = Record, > extends Map { + private readonly [MiddlewareContextSymbol] = true; + + /** + * Check if a value is a {@link MiddlewareContext} instance. + * Works across different package versions in the same realm. + * + * @param value - The value to check. + * @returns Whether the value is a {@link MiddlewareContext} instance. + */ + static isInstance(value: unknown): value is MiddlewareContext { + return isInstance(value, MiddlewareContextSymbol); + } + constructor( - entries?: Iterable, + entries?: + | Iterable + | KeyValues, ) { - super(entries); + super( + entries && isIterable(entries) + ? entries + : entriesFromKeyValues(entries ?? {}), + ); Object.freeze(this); } @@ -69,10 +90,39 @@ export class MiddlewareContext< } } +/** + * {@link Iterable} type guard. + * + * @param value - The value to check. + * @returns Whether the value is an {@link Iterable}. + */ +function isIterable( + value: Iterable | Record, +): value is Iterable { + return Symbol.iterator in value; +} + +/** + * Like Object.entries(), but includes symbol-keyed properties. + * + * @template KeyValues - The type of the keys and values in the object. + * @param keyValues - The object to convert. + * @returns The array of entries, including symbol-keyed properties. + */ +function entriesFromKeyValues>( + keyValues: KeyValues, +): [keyof KeyValues, KeyValues[keyof KeyValues]][] { + return Reflect.ownKeys(keyValues).map((key: keyof KeyValues) => [ + key, + keyValues[key], + ]); +} + /** * Infer the KeyValues type from a {@link MiddlewareContext}. */ -type InferKeyValues = T extends MiddlewareContext ? U : never; +export type InferKeyValues = + T extends MiddlewareContext ? U : never; /** * Simplifies an object type by "merging" its properties. diff --git a/packages/json-rpc-engine/src/v2/index.ts b/packages/json-rpc-engine/src/v2/index.ts index ba2c932e428..393b4244bd6 100644 --- a/packages/json-rpc-engine/src/v2/index.ts +++ b/packages/json-rpc-engine/src/v2/index.ts @@ -4,6 +4,7 @@ export { createScaffoldMiddleware } from './createScaffoldMiddleware'; export { JsonRpcEngineV2 } from './JsonRpcEngineV2'; export type { JsonRpcMiddleware, + HandleOptions, MergedContextOf, MiddlewareParams, MiddlewareConstraint, diff --git a/packages/json-rpc-engine/src/v2/utils.test.ts b/packages/json-rpc-engine/src/v2/utils.test.ts index f7fde4f0e05..748713656cb 100644 --- a/packages/json-rpc-engine/src/v2/utils.test.ts +++ b/packages/json-rpc-engine/src/v2/utils.test.ts @@ -3,6 +3,7 @@ import { isNotification, stringify, JsonRpcEngineError, + isInstance, } from './utils'; const jsonrpc = '2.0' as const; @@ -54,6 +55,30 @@ describe('utils', () => { }); }); + describe('isInstance', () => { + const TestClassSymbol = Symbol('TestClass'); + + class TestClass { + private readonly [TestClassSymbol] = true; + } + + it('identifies class instances via the symbol property', () => { + const value = new TestClass(); + expect(isInstance(value, TestClassSymbol)).toBe(true); + }); + + it('identifies plain objects via the symbol property', () => { + const value = { [TestClassSymbol]: true }; + expect(isInstance(value, TestClassSymbol)).toBe(true); + }); + + it('identifies sub-classes of the class via the symbol property', () => { + class SubClass extends TestClass {} + const value = new SubClass(); + expect(isInstance(value, TestClassSymbol)).toBe(true); + }); + }); + describe('JsonRpcEngineError', () => { it('creates an error with the correct name', () => { const error = new JsonRpcEngineError('test'); @@ -62,8 +87,9 @@ describe('utils', () => { expect(error.message).toBe('test'); }); - it('isInstance checks if a value is a JsonRpcEngineError instance', () => { + it('identifies JsonRpcEngineError instances via isInstance', () => { const error = new JsonRpcEngineError('test'); + expect(JsonRpcEngineError.isInstance(error)).toBe(true); expect(JsonRpcEngineError.isInstance(new Error('test'))).toBe(false); }); diff --git a/packages/json-rpc-engine/src/v2/utils.ts b/packages/json-rpc-engine/src/v2/utils.ts index 345c9907b18..b324034d4ad 100644 --- a/packages/json-rpc-engine/src/v2/utils.ts +++ b/packages/json-rpc-engine/src/v2/utils.ts @@ -1,5 +1,6 @@ import { hasProperty, + isObject, type JsonRpcNotification, type JsonRpcParams, type JsonRpcRequest, @@ -48,7 +49,22 @@ export function stringify(value: unknown): string { return JSON.stringify(value, null, 2); } -const JsonRpcEngineErrorSymbol = Symbol.for('JsonRpcEngineError'); +/** + * The implementation of static `isInstance` methods for classes that have them. + * + * @param value - The value to check. + * @param symbol - The symbol property to check for. + * @returns Whether the value has `{ [symbol]: true }` in its prototype chain. + */ +export const isInstance = ( + value: unknown, + symbol: symbol, +): value is { [key: symbol]: true } => + isObject(value) && symbol in value && value[symbol] === true; + +const JsonRpcEngineErrorSymbol = Symbol.for( + 'json-rpc-engine#JsonRpcEngineError', +); export class JsonRpcEngineError extends Error { private readonly [JsonRpcEngineErrorSymbol] = true; @@ -65,12 +81,7 @@ export class JsonRpcEngineError extends Error { * @param value - The value to check. * @returns Whether the value is a {@link JsonRpcEngineError} instance. */ - static isInstance( - value: Value, - ): value is Value & JsonRpcEngineError { - return ( - hasProperty(value, JsonRpcEngineErrorSymbol) && - value[JsonRpcEngineErrorSymbol] === true - ); + static isInstance(value: unknown): value is JsonRpcEngineError { + return isInstance(value, JsonRpcEngineErrorSymbol); } } diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 8e2ec5d1614..518ae057ea3 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Use `InternalProvider` instead of `SafeEventEmitterProvider` ([#6796](https://github.com/MetaMask/core/pull/6796)) - Providers accessible either via network clients or global proxies no longer emit events (or inherit from EventEmitter, for that matter). +- **BREAKING:** Make `Provider` type more specific ([#7061](https://github.com/MetaMask/core/pull/7061)) + - The `Provider` type is now an `InternalProvider` with a context type of `{ origin: string; skipCache: boolean } & Record`. - **BREAKING:** Stop retrying `undefined` results for methods that include a block tag parameter ([#7001](https://github.com/MetaMask/core/pull/7001)) - The network client middleware, via `@metamask/eth-json-rpc-middleware`, will now throw an error if it encounters an `undefined` result when dispatching a request with a later block number than the originally requested block number. diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index c4cb46ec4de..bc95fdd38eb 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -173,7 +173,7 @@ export function createNetworkClient({ engine.push(networkMiddleware); - const provider = new InternalProvider({ engine }); + const provider: Provider = new InternalProvider({ engine }); const destroy = () => { // TODO: Either fix this lint violation or explain why it's necessary to ignore. diff --git a/packages/network-controller/src/types.ts b/packages/network-controller/src/types.ts index bcc0894f6ea..00c5283b46e 100644 --- a/packages/network-controller/src/types.ts +++ b/packages/network-controller/src/types.ts @@ -1,9 +1,14 @@ import type { InfuraNetworkType, ChainId } from '@metamask/controller-utils'; import type { BlockTracker as BaseBlockTracker } from '@metamask/eth-block-tracker'; import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; +import type { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; import type { Hex } from '@metamask/utils'; -export type Provider = InternalProvider; +export type Provider = InternalProvider< + MiddlewareContext< + { origin: string; skipCache: boolean } & Record + > +>; export type BlockTracker = BaseBlockTracker & { checkForLatestBlock(): Promise; diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 77be127ecbd..18cbac232a2 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -16639,7 +16639,9 @@ function buildFakeClient( rpcUrl: 'https://test.network', }, provider, - blockTracker: new FakeBlockTracker({ provider }), + blockTracker: new FakeBlockTracker({ + provider, + }), destroy: () => { // do nothing }, diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index a709fb799c6..5cb3735fbb0 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -6,6 +6,7 @@ import { toHex, } from '@metamask/controller-utils'; import type { ErrorReportingServiceCaptureExceptionAction } from '@metamask/error-reporting-service'; +import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; import { Messenger, type MockAnyNamespace, @@ -154,7 +155,9 @@ function buildFakeNetworkClient({ return { configuration, provider, - blockTracker: new FakeBlockTracker({ provider }), + blockTracker: new FakeBlockTracker({ + provider: provider as unknown as InternalProvider, + }), destroy: () => { // do nothing }, diff --git a/packages/network-controller/tests/network-client/helpers.ts b/packages/network-controller/tests/network-client/helpers.ts index 3d8dc8d7aa1..bc6fface4ba 100644 --- a/packages/network-controller/tests/network-client/helpers.ts +++ b/packages/network-controller/tests/network-client/helpers.ts @@ -2,7 +2,6 @@ import type { JSONRPCResponse } from '@json-rpc-specification/meta-schema'; import type { InfuraNetworkType } from '@metamask/controller-utils'; import { BUILT_IN_NETWORKS } from '@metamask/controller-utils'; import type { BlockTracker } from '@metamask/eth-block-tracker'; -import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; import EthQuery from '@metamask/eth-query'; import type { Hex } from '@metamask/utils'; import nock, { isDone as nockIsDone } from 'nock'; @@ -11,7 +10,7 @@ import { useFakeTimers } from 'sinon'; import { createNetworkClient } from '../../src/create-network-client'; import type { NetworkControllerOptions } from '../../src/NetworkController'; -import type { NetworkClientConfiguration } from '../../src/types'; +import type { NetworkClientConfiguration, Provider } from '../../src/types'; import { NetworkClientType } from '../../src/types'; import type { RootMessenger } from '../helpers'; import { @@ -390,7 +389,7 @@ export async function withMockedCommunications( type MockNetworkClient = { blockTracker: BlockTracker; - provider: InternalProvider; + provider: Provider; clock: sinon.SinonFakeTimers; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/tests/fake-block-tracker.ts b/tests/fake-block-tracker.ts index d1313aa175c..76b29df8af0 100644 --- a/tests/fake-block-tracker.ts +++ b/tests/fake-block-tracker.ts @@ -1,14 +1,20 @@ import { PollingBlockTracker } from '@metamask/eth-block-tracker'; import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; +import type { + ContextConstraint, + MiddlewareContext, +} from '@metamask/json-rpc-engine/v2'; /** * Acts like a PollingBlockTracker, but doesn't start the polling loop or * make any requests. */ -export class FakeBlockTracker extends PollingBlockTracker { +export class FakeBlockTracker< + Context extends ContextConstraint = MiddlewareContext, +> extends PollingBlockTracker { #latestBlockNumber = '0x0'; - constructor({ provider }: { provider: InternalProvider }) { + constructor({ provider }: { provider: InternalProvider }) { super({ provider, }); diff --git a/tests/fake-provider.ts b/tests/fake-provider.ts index 7d30aa46216..6122dbe681b 100644 --- a/tests/fake-provider.ts +++ b/tests/fake-provider.ts @@ -1,9 +1,16 @@ import { InternalProvider } from '@metamask/eth-json-rpc-provider'; -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngineV2, + type JsonRpcMiddleware, + type MiddlewareContext, + type ResultConstraint, +} from '@metamask/json-rpc-engine/v2'; +import type { Provider } from '@metamask/network-controller'; import type { Json, JsonRpcId, JsonRpcParams, + JsonRpcRequest, JsonRpcResponse, JsonRpcVersion2, } from '@metamask/utils'; @@ -91,6 +98,15 @@ type FakeProviderEngineOptions = { stubs?: FakeProviderStub[]; }; +type Context = MiddlewareContext< + { origin: string; skipCache: boolean } & Record +>; +type Middleware = JsonRpcMiddleware< + JsonRpcRequest, + ResultConstraint, + Context +>; + /** * An implementation of the provider that NetworkController exposes, which is * actually an instance of InternalProvider (from the @@ -102,7 +118,10 @@ type FakeProviderEngineOptions = { // NOTE: We shouldn't need to extend from the "real" provider here, but // we'd need a `InternalProvider` _interface_ and that doesn't exist (at // least not yet). -export class FakeProvider extends InternalProvider { +export class FakeProvider + extends InternalProvider + implements Provider +{ calledStubs: FakeProviderStub[]; #originalStubs: FakeProviderStub[]; @@ -117,7 +136,15 @@ export class FakeProvider extends InternalProvider { * of specific invocations of `request` matching a `method`. */ constructor({ stubs = [] }: FakeProviderEngineOptions = {}) { - super({ engine: new JsonRpcEngine() }); + super({ + engine: JsonRpcEngineV2.create({ + middleware: [ + () => { + throw new Error('FakeProvider received unstubbed method call'); + }, + ], + }), + }); this.#originalStubs = stubs; this.#stubs = this.#originalStubs.slice(); this.calledStubs = []; diff --git a/yarn.lock b/yarn.lock index 8c7587582b9..5bf138de969 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3210,6 +3210,7 @@ __metadata: "@metamask/eth-block-tracker": "npm:^14.0.0" "@metamask/eth-json-rpc-provider": "npm:^5.0.1" "@metamask/json-rpc-engine": "npm:^10.1.1" + "@metamask/network-controller": "npm:^25.0.0" "@metamask/utils": "npm:^11.8.1" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^27.4.1"