diff --git a/package.json b/package.json index ab7bc81..f665bfb 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@metamask/safe-event-emitter": "^2.0.0", + "@metamask/utils": "^2.0.0", "eth-rpc-errors": "^4.0.2" }, "devDependencies": { diff --git a/src/JsonRpcEngine.ts b/src/JsonRpcEngine.ts index 57c7991..8f787b4 100644 --- a/src/JsonRpcEngine.ts +++ b/src/JsonRpcEngine.ts @@ -1,69 +1,19 @@ import SafeEventEmitter from '@metamask/safe-event-emitter'; +import { + hasProperty, + JsonRpcError, + JsonRpcRequest, + JsonRpcResponse, +} from '@metamask/utils'; import { errorCodes, EthereumRpcError, serializeError } from 'eth-rpc-errors'; -type Maybe = Partial | null | undefined; - -export type Json = - | boolean - | number - | string - | null - | { [property: string]: Json } - | Json[]; - -/** - * A String specifying the version of the JSON-RPC protocol. - * MUST be exactly "2.0". - */ -export type JsonRpcVersion = '2.0'; - -/** - * An identifier established by the Client that MUST contain a String, Number, - * or NULL value if included. If it is not included it is assumed to be a - * notification. The value SHOULD normally not be Null and Numbers SHOULD - * NOT contain fractional parts. - */ -export type JsonRpcId = number | string | null; - -export interface JsonRpcError { - code: number; - message: string; - data?: unknown; - stack?: string; -} - -export interface JsonRpcRequest { - jsonrpc: JsonRpcVersion; - method: string; - id: JsonRpcId; - params?: T; -} - -export interface JsonRpcNotification { - jsonrpc: JsonRpcVersion; - method: string; - params?: T; -} - -interface JsonRpcResponseBase { - jsonrpc: JsonRpcVersion; - id: JsonRpcId; -} - -export interface JsonRpcSuccess extends JsonRpcResponseBase { - result: Maybe; -} - -export interface JsonRpcFailure extends JsonRpcResponseBase { - error: JsonRpcError; -} - -export type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure; - -export interface PendingJsonRpcResponse extends JsonRpcResponseBase { - result?: T; - error?: Error | JsonRpcError; -} +export type PendingJsonRpcResponse = Omit< + JsonRpcResponse, + 'error' | 'result' +> & { + result?: Result; + error?: JsonRpcError; +}; export type JsonRpcEngineCallbackError = Error | JsonRpcError | null; @@ -79,9 +29,9 @@ export type JsonRpcEngineEndCallback = ( error?: JsonRpcEngineCallbackError, ) => void; -export type JsonRpcMiddleware = ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, +export type JsonRpcMiddleware = ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, next: JsonRpcEngineNextCallback, end: JsonRpcEngineEndCallback, ) => void; @@ -103,7 +53,7 @@ export class JsonRpcEngine extends SafeEventEmitter { * * @param middleware - The middleware function to add. */ - push(middleware: JsonRpcMiddleware): void { + push(middleware: JsonRpcMiddleware): void { this._middleware.push(middleware as JsonRpcMiddleware); } @@ -113,9 +63,9 @@ export class JsonRpcEngine extends SafeEventEmitter { * @param request - The request to handle. * @param callback - An error-first callback that will receive the response. */ - handle( - request: JsonRpcRequest, - callback: (error: unknown, response: JsonRpcResponse) => void, + handle( + request: JsonRpcRequest, + callback: (error: unknown, response: JsonRpcResponse) => void, ): void; /** @@ -125,9 +75,9 @@ export class JsonRpcEngine extends SafeEventEmitter { * @param callback - An error-first callback that will receive the array of * responses. */ - handle( - requests: JsonRpcRequest[], - callback: (error: unknown, responses: JsonRpcResponse[]) => void, + handle( + requests: JsonRpcRequest[], + callback: (error: unknown, responses: JsonRpcResponse[]) => void, ): void; /** @@ -136,7 +86,9 @@ export class JsonRpcEngine extends SafeEventEmitter { * @param request - The JSON-RPC request to handle. * @returns The JSON-RPC response. */ - handle(request: JsonRpcRequest): Promise>; + handle( + request: JsonRpcRequest, + ): Promise>; /** * Handle an array of JSON-RPC requests, and return an array of responses. @@ -144,7 +96,9 @@ export class JsonRpcEngine extends SafeEventEmitter { * @param request - The JSON-RPC requests to handle. * @returns An array of JSON-RPC responses. */ - handle(requests: JsonRpcRequest[]): Promise[]>; + handle( + requests: JsonRpcRequest[], + ): Promise[]>; handle(req: unknown, callback?: any) { if (callback && typeof callback !== 'function') { @@ -487,7 +441,7 @@ export class JsonRpcEngine extends SafeEventEmitter { res: PendingJsonRpcResponse, isComplete: boolean, ): void { - if (!('result' in res) && !('error' in res)) { + if (!hasProperty(res, 'result') && !hasProperty(res, 'error')) { throw new EthereumRpcError( errorCodes.rpc.internal, `JsonRpcEngine: Response has no error or result for request:\n${jsonify( diff --git a/src/asMiddleware.test.ts b/src/asMiddleware.test.ts index 3b012ab..bb575d0 100644 --- a/src/asMiddleware.test.ts +++ b/src/asMiddleware.test.ts @@ -1,9 +1,9 @@ import { assertIsJsonRpcSuccess, isJsonRpcSuccess, - JsonRpcEngine, JsonRpcRequest, -} from '.'; +} from '@metamask/utils'; +import { JsonRpcEngine } from '.'; const jsonrpc = '2.0' as const; diff --git a/src/createAsyncMiddleware.test.ts b/src/createAsyncMiddleware.test.ts index b087754..d87c9ec 100644 --- a/src/createAsyncMiddleware.test.ts +++ b/src/createAsyncMiddleware.test.ts @@ -1,8 +1,5 @@ -import { - JsonRpcEngine, - createAsyncMiddleware, - assertIsJsonRpcSuccess, -} from '.'; +import { assertIsJsonRpcSuccess } from '@metamask/utils'; +import { JsonRpcEngine, createAsyncMiddleware } from '.'; const jsonrpc = '2.0' as const; diff --git a/src/createAsyncMiddleware.ts b/src/createAsyncMiddleware.ts index 5f60ef9..7d30675 100644 --- a/src/createAsyncMiddleware.ts +++ b/src/createAsyncMiddleware.ts @@ -1,14 +1,11 @@ -import { - JsonRpcMiddleware, - JsonRpcRequest, - PendingJsonRpcResponse, -} from './JsonRpcEngine'; +import { JsonRpcRequest } from '@metamask/utils'; +import { JsonRpcMiddleware, PendingJsonRpcResponse } from './JsonRpcEngine'; export type AsyncJsonRpcEngineNextCallback = () => Promise; -export type AsyncJsonrpcMiddleware = ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, +export type AsyncJsonrpcMiddleware = ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, next: AsyncJsonRpcEngineNextCallback, ) => Promise; @@ -35,9 +32,9 @@ type ReturnHandlerCallback = (error: null | Error) => void; * @returns The wrapped asynchronous middleware function, ready to be consumed * by JsonRpcEngine. */ -export function createAsyncMiddleware( - asyncMiddleware: AsyncJsonrpcMiddleware, -): JsonRpcMiddleware { +export function createAsyncMiddleware( + asyncMiddleware: AsyncJsonrpcMiddleware, +): JsonRpcMiddleware { return async (req, res, next, end) => { // nextPromise is the key to the implementation // it is resolved by the return handler passed to the diff --git a/src/createScaffoldMiddleware.test.ts b/src/createScaffoldMiddleware.test.ts index 4b45a48..5bdf2c3 100644 --- a/src/createScaffoldMiddleware.test.ts +++ b/src/createScaffoldMiddleware.test.ts @@ -1,10 +1,9 @@ import { - JsonRpcEngine, - createScaffoldMiddleware, - JsonRpcMiddleware, assertIsJsonRpcSuccess, assertIsJsonRpcFailure, -} from '.'; +} from '@metamask/utils'; +import { ethErrors } from 'eth-rpc-errors'; +import { JsonRpcEngine, createScaffoldMiddleware, JsonRpcMiddleware } from '.'; describe('createScaffoldMiddleware', () => { it('basic middleware test', async () => { @@ -20,7 +19,7 @@ describe('createScaffoldMiddleware', () => { end(); }, method3: (_req, res, _next, end) => { - res.error = new Error('method3'); + res.error = ethErrors.rpc.internal({ message: 'method3' }); end(); }, }; diff --git a/src/createScaffoldMiddleware.ts b/src/createScaffoldMiddleware.ts index f728dc1..a9404d7 100644 --- a/src/createScaffoldMiddleware.ts +++ b/src/createScaffoldMiddleware.ts @@ -1,6 +1,9 @@ -import { Json, JsonRpcMiddleware, JsonRpcSuccess } from './JsonRpcEngine'; +import { Json, JsonRpcSuccess } from '@metamask/utils'; +import { JsonRpcMiddleware } from './JsonRpcEngine'; -type ScaffoldMiddlewareHandler = JsonRpcMiddleware | Json; +type ScaffoldMiddlewareHandler = + | JsonRpcMiddleware + | Json; /** * Creates a middleware function from an object of RPC method handler functions, diff --git a/src/engine.test.ts b/src/engine.test.ts index f0b0a88..9ea39ea 100644 --- a/src/engine.test.ts +++ b/src/engine.test.ts @@ -1,9 +1,11 @@ -import { isJsonRpcFailure, isJsonRpcSuccess } from './utils'; import { - JsonRpcEngine, assertIsJsonRpcSuccess, assertIsJsonRpcFailure, -} from '.'; + isJsonRpcFailure, + isJsonRpcSuccess, +} from '@metamask/utils'; +import { ethErrors } from 'eth-rpc-errors'; +import { JsonRpcEngine } from '.'; const jsonrpc = '2.0' as const; @@ -187,7 +189,7 @@ describe('JsonRpcEngine', () => { const engine = new JsonRpcEngine(); engine.push(function (_req, res, next, _end) { - res.error = new Error('no bueno'); + res.error = ethErrors.rpc.internal({ message: 'foobar' }); next(); }); @@ -208,7 +210,7 @@ describe('JsonRpcEngine', () => { const engine = new JsonRpcEngine(); engine.push(function (_req, res, _next, end) { - res.error = new Error('no bueno'); + res.error = ethErrors.rpc.internal({ message: 'foobar' }); end(); }); @@ -270,7 +272,7 @@ describe('JsonRpcEngine', () => { engine.push(function (req, res, _next, end) { if (req.id === 4) { delete res.result; - res.error = new Error('foobar'); + res.error = ethErrors.rpc.internal({ message: 'foobar' }); return end(res.error); } res.result = req.id; @@ -305,7 +307,7 @@ describe('JsonRpcEngine', () => { engine.push(function (req, res, _next, end) { if (req.id === 4) { delete res.result; - res.error = new Error('foobar'); + res.error = ethErrors.rpc.internal({ message: 'foobar' }); return end(res.error); } res.result = req.id; diff --git a/src/index.ts b/src/index.ts index e22c0ec..d7644d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,4 +4,3 @@ export * from './getUniqueId'; export * from './idRemapMiddleware'; export * from './JsonRpcEngine'; export * from './mergeMiddleware'; -export * from './utils'; diff --git a/src/mergeMiddleware.test.ts b/src/mergeMiddleware.test.ts index 46b09c5..9cbc61e 100644 --- a/src/mergeMiddleware.test.ts +++ b/src/mergeMiddleware.test.ts @@ -1,9 +1,9 @@ import { assertIsJsonRpcSuccess, - JsonRpcEngine, + hasProperty, JsonRpcRequest, - mergeMiddleware, -} from '.'; +} from '@metamask/utils'; +import { JsonRpcEngine, mergeMiddleware } from '.'; const jsonrpc = '2.0' as const; @@ -30,7 +30,7 @@ describe('mergeMiddleware', () => { expect(res).toBeDefined(); expect(originalReq.id).toStrictEqual(res.id); expect(originalReq.jsonrpc).toStrictEqual(res.jsonrpc); - expect('result' in res).toBe(true); + expect(hasProperty(res, 'result')).toBe(true); resolve(); }); }); diff --git a/src/utils.test.ts b/src/utils.test.ts deleted file mode 100644 index e37a30b..0000000 --- a/src/utils.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { - isJsonRpcFailure, - isJsonRpcSuccess, - getJsonRpcIdValidator, - assertIsJsonRpcSuccess, - assertIsJsonRpcFailure, -} from '.'; - -describe('isJsonRpcSuccess', () => { - it('correctly identifies JSON-RPC response objects', () => { - ( - [ - [{ result: 'success' }, true], - [{ result: null }, true], - [{ error: new Error('foo') }, false], - [{}, false], - ] as [any, boolean][] - ).forEach(([input, expectedResult]) => { - expect(isJsonRpcSuccess(input)).toBe(expectedResult); - }); - }); -}); - -describe('isJsonRpcFailure', () => { - it('correctly identifies JSON-RPC response objects', () => { - ( - [ - [{ error: 'failure' }, true], - [{ error: null }, true], - [{ result: 'success' }, false], - [{}, false], - ] as [any, boolean][] - ).forEach(([input, expectedResult]) => { - expect(isJsonRpcFailure(input)).toBe(expectedResult); - }); - }); -}); - -describe('assertIsJsonRpcSuccess', () => { - it('correctly identifies JSON-RPC response objects', () => { - ([{ result: 'success' }, { result: null }] as any[]).forEach((input) => { - expect(() => assertIsJsonRpcSuccess(input)).not.toThrow(); - }); - - ([{ error: new Error('foo') }, {}] as any[]).forEach((input) => { - expect(() => assertIsJsonRpcSuccess(input)).toThrow( - 'Not a successful JSON-RPC response.', - ); - }); - }); -}); - -describe('assertIsJsonRpcFailure', () => { - it('correctly identifies JSON-RPC response objects', () => { - ([{ error: 'failure' }, { error: null }] as any[]).forEach((input) => { - expect(() => assertIsJsonRpcFailure(input)).not.toThrow(); - }); - - ([{ result: 'success' }, {}] as any[]).forEach((input) => { - expect(() => assertIsJsonRpcFailure(input)).toThrow( - 'Not a failed JSON-RPC response.', - ); - }); - }); -}); - -describe('getJsonRpcIdValidator', () => { - const getInputs = () => { - return { - // invariant with respect to options - fractionString: { value: '1.2', expected: true }, - negativeInteger: { value: -1, expected: true }, - object: { value: {}, expected: false }, - positiveInteger: { value: 1, expected: true }, - string: { value: 'foo', expected: true }, - undefined: { value: undefined, expected: false }, - zero: { value: 0, expected: true }, - // variant with respect to options - emptyString: { value: '', expected: true }, - fraction: { value: 1.2, expected: false }, - null: { value: null, expected: true }, - }; - }; - - const validateAll = ( - validate: ReturnType, - inputs: ReturnType, - ) => { - for (const input of Object.values(inputs)) { - expect(validate(input.value)).toStrictEqual(input.expected); - } - }; - - it('performs as expected with default options', () => { - const inputs = getInputs(); - - // The default options are: - // permitEmptyString: true, - // permitFractions: false, - // permitNull: true, - expect(() => validateAll(getJsonRpcIdValidator(), inputs)).not.toThrow(); - }); - - it('performs as expected with "permitEmptyString: false"', () => { - const inputs = getInputs(); - inputs.emptyString.expected = false; - - expect(() => - validateAll( - getJsonRpcIdValidator({ - permitEmptyString: false, - }), - inputs, - ), - ).not.toThrow(); - }); - - it('performs as expected with "permitFractions: true"', () => { - const inputs = getInputs(); - inputs.fraction.expected = true; - - expect(() => - validateAll( - getJsonRpcIdValidator({ - permitFractions: true, - }), - inputs, - ), - ).not.toThrow(); - }); - - it('performs as expected with "permitNull: false"', () => { - const inputs = getInputs(); - inputs.null.expected = false; - - expect(() => - validateAll( - getJsonRpcIdValidator({ - permitNull: false, - }), - inputs, - ), - ).not.toThrow(); - }); -}); diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 9602cd9..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { - JsonRpcFailure, - JsonRpcId, - JsonRpcResponse, - JsonRpcSuccess, -} from './JsonRpcEngine'; - -export const hasProperty = ( - object: Object, // eslint-disable-line @typescript-eslint/ban-types - name: string | number | symbol, -): boolean => Object.hasOwnProperty.call(object, name); - -/** - * ATTN: Assumes that only one of the `result` and `error` properties is - * present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`. - * - * Type guard to narrow a JsonRpcResponse object to a success (or failure). - * - * @param response - The response object to check. - * @returns Whether the response object is a success, i.e. has a `result` - * property. - */ -export function isJsonRpcSuccess( - response: JsonRpcResponse, -): response is JsonRpcSuccess { - return hasProperty(response, 'result'); -} - -/** - * ATTN: Assumes that only one of the `result` and `error` properties is - * present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`. - * - * Type assertion to narrow a JsonRpcResponse object to a success (or failure). - * - * @param response - The response object to check. - */ -export function assertIsJsonRpcSuccess( - response: JsonRpcResponse, -): asserts response is JsonRpcSuccess { - if (!isJsonRpcSuccess(response)) { - throw new Error('Not a successful JSON-RPC response.'); - } -} - -/** - * ATTN: Assumes that only one of the `result` and `error` properties is - * present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`. - * - * Type guard to narrow a JsonRpcResponse object to a failure (or success). - * - * @param response - The response object to check. - * @returns Whether the response object is a failure, i.e. has an `error` - * property. - */ -export function isJsonRpcFailure( - response: JsonRpcResponse, -): response is JsonRpcFailure { - return hasProperty(response, 'error'); -} - -/** - * ATTN: Assumes that only one of the `result` and `error` properties is - * present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`. - * - * Type assertion to narrow a JsonRpcResponse object to a failure (or success). - * - * @param response - The response object to check. - */ -export function assertIsJsonRpcFailure( - response: JsonRpcResponse, -): asserts response is JsonRpcFailure { - if (!isJsonRpcFailure(response)) { - throw new Error('Not a failed JSON-RPC response.'); - } -} - -interface JsonRpcValidatorOptions { - permitEmptyString?: boolean; - permitFractions?: boolean; - permitNull?: boolean; -} - -const DEFAULT_VALIDATOR_OPTIONS: JsonRpcValidatorOptions = { - permitEmptyString: true, - permitFractions: false, - permitNull: true, -}; - -/** - * Gets a function for validating JSON-RPC request / response `id` values. - * - * By manipulating the options of this factory, you can control the behavior - * of the resulting validator for some edge cases. This is useful because e.g. - * `null` should sometimes but not always be permitted. - * - * Note that the empty string (`''`) is always permitted by the JSON-RPC - * specification, but that kind of sucks and you may want to forbid it in some - * instances anyway. - * - * For more details, see the - * [JSON-RPC Specification](https://www.jsonrpc.org/specification). - * - * @param options - An options object. - * @param options.permitEmptyString - Whether the empty string (i.e. `''`) - * should be treated as a valid ID. Default: `true` - * @param options.permitFractions - Whether fractional numbers (e.g. `1.2`) - * should be treated as valid IDs. Default: `false` - * @param options.permitNull - Whether `null` should be treated as a valid ID. - * Default: `true` - * @returns The JSON-RPC ID validator function. - */ -export function getJsonRpcIdValidator(options?: JsonRpcValidatorOptions) { - const { permitEmptyString, permitFractions, permitNull } = { - ...DEFAULT_VALIDATOR_OPTIONS, - ...options, - }; - - /** - * Type guard for {@link JsonRpcId}. - * - * @param id - The JSON-RPC ID value to check. - * @returns Whether the given ID is valid per the options given to the - * factory. - */ - const isValidJsonRpcId = (id: unknown): id is JsonRpcId => { - return Boolean( - (typeof id === 'number' && (permitFractions || Number.isInteger(id))) || - (typeof id === 'string' && (permitEmptyString || id.length > 0)) || - (permitNull && id === null), - ); - }; - return isValidJsonRpcId; -} diff --git a/yarn.lock b/yarn.lock index 3db941c..306d42b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -622,6 +622,13 @@ resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q== +"@metamask/utils@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-2.0.0.tgz#fe7e970416a256751c429f4a5e96aec6c4366ba7" + integrity sha512-AZ63AhRxAZXll+/SEiyEXgrxuAL4yOj0ny4V36VgPmTDvt+7GrmVJWrQF3o5PZZWV6ooaHZ9291RZHRcKZm0HA== + dependencies: + fast-deep-equal "^3.1.3" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"