From 578fb3333547e7ce4db0e4544024a3ca2c1c604a Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 27 Oct 2022 16:08:21 +0200 Subject: [PATCH 01/15] Fix error serialization --- src/utils.ts | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 6a1dd71..10c6707 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,8 +3,7 @@ import { errorCodes, errorValues } from './error-constants'; import { EthereumRpcError, SerializedEthereumRpcError } from './classes'; const FALLBACK_ERROR_CODE = errorCodes.rpc.internal; -const FALLBACK_MESSAGE = - 'Unspecified error message. This is a bug, please report it.'; +const FALLBACK_MESSAGE = 'Invalid internal error. See "data.originalError" for original value. Please report this bug.'; const FALLBACK_ERROR: SerializedEthereumRpcError = { code: FALLBACK_ERROR_CODE, message: getMessageFromCode(FALLBACK_ERROR_CODE), @@ -28,7 +27,7 @@ export function getMessageFromCode( code: number, fallbackMessage: string = FALLBACK_MESSAGE, ): string { - if (Number.isInteger(code)) { + if (isValidCode(code)) { const codeString = code.toString(); if (hasProperty(errorValues, codeString)) { @@ -44,26 +43,13 @@ export function getMessageFromCode( /** * Returns whether the given code is valid. - * A code is only valid if it has a message. + * A code is valid if it is an integer. * * @param code - The error code. * @returns Whether the given code is valid. */ export function isValidCode(code: number): boolean { - if (!Number.isInteger(code)) { - return false; - } - - const codeString = code.toString(); - if (errorValues[codeString as ErrorValueKey]) { - return true; - } - - if (isJsonRpcServerError(code)) { - return true; - } - - return false; + return Number.isInteger(code); } /** @@ -86,7 +72,7 @@ export function serializeError( ): SerializedEthereumRpcError { if ( !fallbackError || - !Number.isInteger(fallbackError.code) || + !isValidCode(fallbackError.code) || typeof fallbackError.message !== 'string' ) { throw new Error( @@ -163,6 +149,5 @@ function assignOriginalError(error: unknown): unknown { if (error && typeof error === 'object' && !Array.isArray(error)) { return Object.assign({}, error); } - return error; } From 3f8d1ce54a7f3a07c01dd817f1b32b0a1e8b527b Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 2 Nov 2022 12:09:16 +0100 Subject: [PATCH 02/15] Fix tests --- jest.config.js | 6 +++--- src/__fixtures__/errors.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jest.config.js b/jest.config.js index 88805a1..e586fb6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,10 +41,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 90.09, + branches: 91.66, functions: 84.21, - lines: 95.32, - statements: 95.32, + lines: 95.83, + statements: 95.83, }, }, diff --git a/src/__fixtures__/errors.ts b/src/__fixtures__/errors.ts index 47a4043..25472d2 100644 --- a/src/__fixtures__/errors.ts +++ b/src/__fixtures__/errors.ts @@ -5,7 +5,7 @@ export const dummyMessage = 'baz'; export const invalidError0 = 0; export const invalidError1 = ['foo', 'bar', 3]; -export const invalidError2 = { code: 34 }; +export const invalidError2 = { code: 'foo' }; export const invalidError3 = { code: 4001 }; export const invalidError4 = { code: 4001, @@ -15,7 +15,7 @@ export const invalidError4 = { export const invalidError5 = null; export const invalidError6 = undefined; export const invalidError7 = { - code: 34, + code: 'foo', message: dummyMessage, data: Object.assign({}, dummyData), }; From 181f2e2135ed6563ae564f49b20e866dff105202 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 2 Nov 2022 12:39:15 +0100 Subject: [PATCH 03/15] Potential implementation --- package.json | 3 +- src/utils.ts | 97 +++++++++++++++++++++------------------------------- yarn.lock | 3 +- 3 files changed, 43 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 1eba820..e52f9fe 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ }, "dependencies": { "@metamask/utils": "^3.2.0", - "fast-safe-stringify": "^2.0.6" + "fast-safe-stringify": "^2.0.6", + "superstruct": "^0.16.6" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.0.3", diff --git a/src/utils.ts b/src/utils.ts index 10c6707..e0af9e2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,16 @@ -import { hasProperty, isPlainObject, Json } from '@metamask/utils'; +import { + hasProperty, + JsonRpcErrorStruct, + isValidJson, + isObject, +} from '@metamask/utils'; +import { is, create } from 'superstruct'; import { errorCodes, errorValues } from './error-constants'; import { EthereumRpcError, SerializedEthereumRpcError } from './classes'; const FALLBACK_ERROR_CODE = errorCodes.rpc.internal; -const FALLBACK_MESSAGE = 'Invalid internal error. See "data.originalError" for original value. Please report this bug.'; +const FALLBACK_MESSAGE = + 'Invalid internal error. See "data.originalError" for original value. Please report this bug.'; const FALLBACK_ERROR: SerializedEthereumRpcError = { code: FALLBACK_ERROR_CODE, message: getMessageFromCode(FALLBACK_ERROR_CODE), @@ -70,11 +77,7 @@ export function serializeError( error: unknown, { fallbackError = FALLBACK_ERROR, shouldIncludeStack = false } = {}, ): SerializedEthereumRpcError { - if ( - !fallbackError || - !isValidCode(fallbackError.code) || - typeof fallbackError.message !== 'string' - ) { + if (!is(fallbackError, JsonRpcErrorStruct)) { throw new Error( 'Must provide fallback error with integer number code and string message.', ); @@ -84,49 +87,40 @@ export function serializeError( return error.serialize(); } - const serialized: Partial = {}; + const serialized = buildError( + error, + fallbackError as SerializedEthereumRpcError, + ); - if ( - error && - isPlainObject(error) && - hasProperty(error, 'code') && - isValidCode((error as SerializedEthereumRpcError).code) - ) { - const _error = error as Partial; - serialized.code = _error.code as number; - - if (_error.message && typeof _error.message === 'string') { - serialized.message = _error.message; - - if (hasProperty(_error, 'data')) { - serialized.data = _error.data ?? null; - } - } else { - serialized.message = getMessageFromCode( - (serialized as SerializedEthereumRpcError).code, - ); - - // TODO: Verify that the original error is serializable. - serialized.data = { originalError: assignOriginalError(error) } as Json; - } - } else { - serialized.code = fallbackError.code; - - const message = (error as any)?.message; - - serialized.message = - message && typeof message === 'string' ? message : fallbackError.message; - - // TODO: Verify that the original error is serializable. - serialized.data = { originalError: assignOriginalError(error) } as Json; + if (!shouldIncludeStack) { + delete serialized.stack; } - const stack = (error as any)?.stack; + return serialized as SerializedEthereumRpcError; +} - if (shouldIncludeStack && error && stack && typeof stack === 'string') { - serialized.stack = stack; +/** + * Constructs a JSON serializable object given an error and a fallbackError. + * + * @param error - The error in question. + * @param fallbackError - The fallback error. + * @returns A JSON serializable error object. + */ +function buildError(error: unknown, fallbackError: SerializedEthereumRpcError) { + if (is(error, JsonRpcErrorStruct)) { + return create(error, JsonRpcErrorStruct); } - return serialized as SerializedEthereumRpcError; + // If the original error is an object, we make a copy of it, this should also copy class properties to an object + const originalError = isObject(error) ? Object.assign({}, error) : error; + const fallbackWithOriginal = { + ...fallbackError, + data: { originalError }, + }; + // We only allow returning originalError if it turns out to be valid JSON + if (isValidJson(fallbackWithOriginal)) { + return fallbackWithOriginal; + } + return fallbackError; } /** @@ -138,16 +132,3 @@ export function serializeError( function isJsonRpcServerError(code: number): boolean { return code >= -32099 && code <= -32000; } - -/** - * Create a copy of the given value if it's an object, and not an array. - * - * @param error - The value to copy. - * @returns The copied value, or the original value if it's not an object. - */ -function assignOriginalError(error: unknown): unknown { - if (error && typeof error === 'object' && !Array.isArray(error)) { - return Object.assign({}, error); - } - return error; -} diff --git a/yarn.lock b/yarn.lock index 99257e6..1bc567b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -911,6 +911,7 @@ __metadata: jest-it-up: ^2.0.2 prettier: ^2.7.1 prettier-plugin-packagejson: ^2.2.18 + superstruct: ^0.16.6 ts-jest: ^28.0.7 typedoc: ^0.23.19 typescript: ~4.7.4 @@ -5554,7 +5555,7 @@ __metadata: languageName: node linkType: hard -"superstruct@npm:^0.16.5": +"superstruct@npm:^0.16.5, superstruct@npm:^0.16.6": version: 0.16.7 resolution: "superstruct@npm:0.16.7" checksum: c8c855ff6945da8a41048c6d236de7b1af5d4d9c31742b3ee54d65647c31597488620281f65e095d5efc9e2fbdaad529b8c8f2506c12569d428467c835a21477 From b29f14aa7cc0c71ffa47b6fb46c0368e7d5f724f Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 2 Nov 2022 16:08:40 +0100 Subject: [PATCH 04/15] Use latest utils --- package.json | 4 ++-- src/utils.ts | 7 ++++--- yarn.lock | 17 ++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index e52f9fe..1e9db04 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ "test:watch": "jest --watch" }, "dependencies": { - "@metamask/utils": "^3.2.0", + "@metamask/utils": "^3.3.0", "fast-safe-stringify": "^2.0.6", - "superstruct": "^0.16.6" + "superstruct": "^0.16.7" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.0.3", diff --git a/src/utils.ts b/src/utils.ts index e0af9e2..ae95499 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,8 +3,9 @@ import { JsonRpcErrorStruct, isValidJson, isObject, + isJsonRpcError, } from '@metamask/utils'; -import { is, create } from 'superstruct'; +import { create } from 'superstruct'; import { errorCodes, errorValues } from './error-constants'; import { EthereumRpcError, SerializedEthereumRpcError } from './classes'; @@ -77,7 +78,7 @@ export function serializeError( error: unknown, { fallbackError = FALLBACK_ERROR, shouldIncludeStack = false } = {}, ): SerializedEthereumRpcError { - if (!is(fallbackError, JsonRpcErrorStruct)) { + if (!isJsonRpcError(fallbackError)) { throw new Error( 'Must provide fallback error with integer number code and string message.', ); @@ -107,7 +108,7 @@ export function serializeError( * @returns A JSON serializable error object. */ function buildError(error: unknown, fallbackError: SerializedEthereumRpcError) { - if (is(error, JsonRpcErrorStruct)) { + if (isJsonRpcError(error)) { return create(error, JsonRpcErrorStruct); } // If the original error is an object, we make a copy of it, this should also copy class properties to an object diff --git a/yarn.lock b/yarn.lock index 1bc567b..48f2ed4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -894,7 +894,7 @@ __metadata: "@metamask/eslint-config-jest": ^10.0.0 "@metamask/eslint-config-nodejs": ^10.0.0 "@metamask/eslint-config-typescript": ^10.0.0 - "@metamask/utils": ^3.2.0 + "@metamask/utils": ^3.3.0 "@types/jest": ^28.1.6 "@typescript-eslint/eslint-plugin": ^5.33.1 "@typescript-eslint/parser": ^5.33.1 @@ -911,22 +911,21 @@ __metadata: jest-it-up: ^2.0.2 prettier: ^2.7.1 prettier-plugin-packagejson: ^2.2.18 - superstruct: ^0.16.6 + superstruct: ^0.16.7 ts-jest: ^28.0.7 typedoc: ^0.23.19 typescript: ~4.7.4 languageName: unknown linkType: soft -"@metamask/utils@npm:^3.2.0": - version: 3.2.0 - resolution: "@metamask/utils@npm:3.2.0" +"@metamask/utils@npm:^3.3.0": + version: 3.3.0 + resolution: "@metamask/utils@npm:3.3.0" dependencies: "@types/debug": ^4.1.7 debug: ^4.3.4 - fast-deep-equal: ^3.1.3 - superstruct: ^0.16.5 - checksum: 99adcbd273c69075628913259f8c3fb843291898eba813f4f5fe0bfc060ae5955e2c69e70e15b04156793f8d84edd077d1fac3f8c3927e067d0f311eef9d4469 + superstruct: ^0.16.7 + checksum: 263c26722b7339fced997cc2aa38ccb34d178f532ff025c7e789818ad5ef2317439eda694a4c0bf5ff3cabdeced3c170b9ad23d2f178ddd5fd225f6ec2bce259 languageName: node linkType: hard @@ -5555,7 +5554,7 @@ __metadata: languageName: node linkType: hard -"superstruct@npm:^0.16.5, superstruct@npm:^0.16.6": +"superstruct@npm:^0.16.7": version: 0.16.7 resolution: "superstruct@npm:0.16.7" checksum: c8c855ff6945da8a41048c6d236de7b1af5d4d9c31742b3ee54d65647c31597488620281f65e095d5efc9e2fbdaad529b8c8f2506c12569d428467c835a21477 From f7b90e42dc7a1a146068cfd35d14bd549b36b769 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 2 Nov 2022 18:09:16 +0100 Subject: [PATCH 05/15] Implement subset error --- src/utils.test.ts | 8 ++++++++ src/utils.ts | 23 +++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 3007598..5b92d58 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -191,4 +191,12 @@ describe('serializeError', () => { data: Object.assign({}, validError1.data), }); }); + + it('handles regular Error()', () => { + const result = serializeError(new Error('foo')); + expect(result).toStrictEqual({ + code: errorCodes.rpc.internal, + message: 'foo', + }); + }); }); diff --git a/src/utils.ts b/src/utils.ts index ae95499..c3594cc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,7 @@ import { isObject, isJsonRpcError, } from '@metamask/utils'; -import { create } from 'superstruct'; +import { create, is, partial } from 'superstruct'; import { errorCodes, errorValues } from './error-constants'; import { EthereumRpcError, SerializedEthereumRpcError } from './classes'; @@ -100,6 +100,10 @@ export function serializeError( return serialized as SerializedEthereumRpcError; } +const PartialJsonRpcErrorStruct = partial(JsonRpcErrorStruct); + +const ERROR_KEYS = Object.keys(JsonRpcErrorStruct.schema); + /** * Constructs a JSON serializable object given an error and a fallbackError. * @@ -111,16 +115,31 @@ function buildError(error: unknown, fallbackError: SerializedEthereumRpcError) { if (isJsonRpcError(error)) { return create(error, JsonRpcErrorStruct); } - // If the original error is an object, we make a copy of it, this should also copy class properties to an object + + // If the original error is a partial JSON-RPC error, extract matching keys and merge with fallback error + if (is(error, PartialJsonRpcErrorStruct)) { + const subset = ERROR_KEYS.reduce((acc, key) => { + if (hasProperty(error, key)) { + // @ts-expect-error TODO: Fix type + acc[key] = error[key]; + } + return acc; + }, {}); + return { ...fallbackError, ...subset }; + } + + // TODO: If the original error is an object, we make a copy of it, this should also copy class properties to an object const originalError = isObject(error) ? Object.assign({}, error) : error; const fallbackWithOriginal = { ...fallbackError, data: { originalError }, }; + // We only allow returning originalError if it turns out to be valid JSON if (isValidJson(fallbackWithOriginal)) { return fallbackWithOriginal; } + return fallbackError; } From 7be7ca46a49a0e549877b9e6dcde700e61dea54d Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 3 Nov 2022 10:14:27 +0100 Subject: [PATCH 06/15] Add cause property --- src/utils.test.ts | 18 +++++++++--------- src/utils.ts | 38 +++++++++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 5b92d58..e0a3569 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -25,7 +25,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.internal, message: getMessageFromCode(rpcCodes.internal), - data: { originalError: invalidError0 }, + data: { cause: invalidError0 }, }); }); @@ -34,7 +34,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.internal, message: getMessageFromCode(rpcCodes.internal), - data: { originalError: invalidError5 }, + data: { cause: invalidError5 }, }); }); @@ -43,7 +43,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.internal, message: getMessageFromCode(rpcCodes.internal), - data: { originalError: invalidError6 }, + data: { cause: invalidError6 }, }); }); @@ -52,7 +52,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.internal, message: getMessageFromCode(rpcCodes.internal), - data: { originalError: invalidError1 }, + data: { cause: invalidError1 }, }); }); @@ -61,7 +61,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.internal, message: getMessageFromCode(rpcCodes.internal), - data: { originalError: invalidError2 }, + data: { cause: invalidError2 }, }); }); @@ -70,7 +70,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: 4001, message: getMessageFromCode(4001), - data: { originalError: Object.assign({}, invalidError3) }, + data: { cause: Object.assign({}, invalidError3) }, }); }); @@ -79,7 +79,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: 4001, message: getMessageFromCode(4001), - data: { originalError: Object.assign({}, invalidError4) }, + data: { cause: Object.assign({}, invalidError4) }, }); }); @@ -88,7 +88,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.internal, message: dummyMessage, - data: { originalError: Object.assign({}, invalidError7) }, + data: { cause: Object.assign({}, invalidError7) }, }); }); @@ -99,7 +99,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.methodNotFound, message: 'foo', - data: { originalError: Object.assign({}, invalidError2) }, + data: { cause: Object.assign({}, invalidError2) }, }); }); diff --git a/src/utils.ts b/src/utils.ts index c3594cc..6019641 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,7 @@ import { isValidJson, isObject, isJsonRpcError, + Json, } from '@metamask/utils'; import { create, is, partial } from 'superstruct'; import { errorCodes, errorValues } from './error-constants'; @@ -11,7 +12,7 @@ import { EthereumRpcError, SerializedEthereumRpcError } from './classes'; const FALLBACK_ERROR_CODE = errorCodes.rpc.internal; const FALLBACK_MESSAGE = - 'Invalid internal error. See "data.originalError" for original value. Please report this bug.'; + 'Invalid internal error. See "data.cause" for original value. Please report this bug.'; const FALLBACK_ERROR: SerializedEthereumRpcError = { code: FALLBACK_ERROR_CODE, message: getMessageFromCode(FALLBACK_ERROR_CODE), @@ -64,7 +65,7 @@ export function isValidCode(code: number): boolean { * Serializes the given error to an Ethereum JSON RPC-compatible error object. * Merely copies the given error's values if it is already compatible. * If the given error is not fully compatible, it will be preserved on the - * returned object's data.originalError property. + * returned object's data.cause property. * * @param error - The error to serialize. * @param options - Options bag. @@ -129,18 +130,13 @@ function buildError(error: unknown, fallbackError: SerializedEthereumRpcError) { } // TODO: If the original error is an object, we make a copy of it, this should also copy class properties to an object - const originalError = isObject(error) ? Object.assign({}, error) : error; - const fallbackWithOriginal = { + const cause = isObject(error) ? getCause(error) : error; + const fallbackWithCause = { ...fallbackError, - data: { originalError }, + data: { cause }, }; - // We only allow returning originalError if it turns out to be valid JSON - if (isValidJson(fallbackWithOriginal)) { - return fallbackWithOriginal; - } - - return fallbackError; + return fallbackWithCause; } /** @@ -152,3 +148,23 @@ function buildError(error: unknown, fallbackError: SerializedEthereumRpcError) { function isJsonRpcServerError(code: number): boolean { return code >= -32099 && code <= -32000; } + +/** + * Get cause from an object or a class, extracting all properties. + * + * @param error - The error class or object. + * @returns An object containing all the error properties. + */ +function getCause(error: T): Json { + return Object.getOwnPropertyNames(error).reduce((acc, key) => { + const value = error[key as keyof T]; + if (isValidJson(value)) { + return { + ...acc, + [key]: value, + }; + } + + return acc; + }, {}); +} From 6e8737b5a2bc5d9b6f7286cd73554e26e22d2539 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 3 Nov 2022 10:25:47 +0100 Subject: [PATCH 07/15] Typing --- src/classes.ts | 13 ++++--------- src/utils.ts | 20 +++++++++----------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/classes.ts b/src/classes.ts index 9a3b2f9..1654ad2 100644 --- a/src/classes.ts +++ b/src/classes.ts @@ -1,12 +1,7 @@ import safeStringify from 'fast-safe-stringify'; -import { Json } from '@metamask/utils'; +import { Json, JsonRpcError } from '@metamask/utils'; -export type SerializedEthereumRpcError = { - code: number; - message: string; - data?: Json; - stack?: string; -}; +export { JsonRpcError }; /** * Error subclass implementing JSON RPC 2.0 errors and Ethereum RPC errors @@ -40,8 +35,8 @@ export class EthereumRpcError extends Error { * * @returns A plain object with all public class properties. */ - serialize(): SerializedEthereumRpcError { - const serialized: SerializedEthereumRpcError = { + serialize(): JsonRpcError { + const serialized: JsonRpcError = { code: this.code, message: this.message, }; diff --git a/src/utils.ts b/src/utils.ts index 6019641..5f34231 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,15 +5,16 @@ import { isObject, isJsonRpcError, Json, + JsonRpcError, } from '@metamask/utils'; import { create, is, partial } from 'superstruct'; import { errorCodes, errorValues } from './error-constants'; -import { EthereumRpcError, SerializedEthereumRpcError } from './classes'; +import { EthereumRpcError } from './classes'; const FALLBACK_ERROR_CODE = errorCodes.rpc.internal; const FALLBACK_MESSAGE = 'Invalid internal error. See "data.cause" for original value. Please report this bug.'; -const FALLBACK_ERROR: SerializedEthereumRpcError = { +const FALLBACK_ERROR: JsonRpcError = { code: FALLBACK_ERROR_CODE, message: getMessageFromCode(FALLBACK_ERROR_CODE), }; @@ -78,7 +79,7 @@ export function isValidCode(code: number): boolean { export function serializeError( error: unknown, { fallbackError = FALLBACK_ERROR, shouldIncludeStack = false } = {}, -): SerializedEthereumRpcError { +): JsonRpcError { if (!isJsonRpcError(fallbackError)) { throw new Error( 'Must provide fallback error with integer number code and string message.', @@ -89,16 +90,13 @@ export function serializeError( return error.serialize(); } - const serialized = buildError( - error, - fallbackError as SerializedEthereumRpcError, - ); + const serialized = buildError(error, fallbackError as JsonRpcError); if (!shouldIncludeStack) { delete serialized.stack; } - return serialized as SerializedEthereumRpcError; + return serialized; } const PartialJsonRpcErrorStruct = partial(JsonRpcErrorStruct); @@ -106,13 +104,13 @@ const PartialJsonRpcErrorStruct = partial(JsonRpcErrorStruct); const ERROR_KEYS = Object.keys(JsonRpcErrorStruct.schema); /** - * Constructs a JSON serializable object given an error and a fallbackError. + * Construct a JSON-serializable object given an error and a `fallbackError` * * @param error - The error in question. * @param fallbackError - The fallback error. * @returns A JSON serializable error object. */ -function buildError(error: unknown, fallbackError: SerializedEthereumRpcError) { +function buildError(error: unknown, fallbackError: JsonRpcError): JsonRpcError { if (isJsonRpcError(error)) { return create(error, JsonRpcErrorStruct); } @@ -136,7 +134,7 @@ function buildError(error: unknown, fallbackError: SerializedEthereumRpcError) { data: { cause }, }; - return fallbackWithCause; + return fallbackWithCause as JsonRpcError; } /** From 42316b895975f6e47466c512803bc7c0cf888614 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 3 Nov 2022 11:11:27 +0100 Subject: [PATCH 08/15] More implementation changes --- jest.config.js | 8 ++++---- src/utils.test.ts | 6 +++--- src/utils.ts | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/jest.config.js b/jest.config.js index e586fb6..5dc0cac 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,10 +41,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 91.66, - functions: 84.21, - lines: 95.83, - statements: 95.83, + branches: 92.3, + functions: 85, + lines: 96.04, + statements: 96.04, }, }, diff --git a/src/utils.test.ts b/src/utils.test.ts index e0a3569..2204a2b 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -13,6 +13,7 @@ import { validError3, validError4, dummyMessage, + dummyData, } from './__fixtures__'; import { getMessageFromCode, serializeError } from './utils'; import { errorCodes } from '.'; @@ -70,7 +71,6 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: 4001, message: getMessageFromCode(4001), - data: { cause: Object.assign({}, invalidError3) }, }); }); @@ -79,7 +79,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: 4001, message: getMessageFromCode(4001), - data: { cause: Object.assign({}, invalidError4) }, + data: Object.assign({}, dummyData), }); }); @@ -88,7 +88,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.internal, message: dummyMessage, - data: { cause: Object.assign({}, invalidError7) }, + data: Object.assign({}, dummyData), }); }); diff --git a/src/utils.ts b/src/utils.ts index 5f34231..f979aed 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,7 @@ import { isJsonRpcError, Json, JsonRpcError, + isNonEmptyArray, } from '@metamask/utils'; import { create, is, partial } from 'superstruct'; import { errorCodes, errorValues } from './error-constants'; @@ -34,7 +35,7 @@ type ErrorValueKey = keyof typeof errorValues; * has no corresponding message. */ export function getMessageFromCode( - code: number, + code: unknown, fallbackMessage: string = FALLBACK_MESSAGE, ): string { if (isValidCode(code)) { @@ -58,7 +59,7 @@ export function getMessageFromCode( * @param code - The error code. * @returns Whether the given code is valid. */ -export function isValidCode(code: number): boolean { +export function isValidCode(code: unknown): code is number { return Number.isInteger(code); } @@ -116,18 +117,25 @@ function buildError(error: unknown, fallbackError: JsonRpcError): JsonRpcError { } // If the original error is a partial JSON-RPC error, extract matching keys and merge with fallback error - if (is(error, PartialJsonRpcErrorStruct)) { + if (isObject(error)) { const subset = ERROR_KEYS.reduce((acc, key) => { - if (hasProperty(error, key)) { + if ( + hasProperty(error, key) && + // @ts-expect-error TODO: Fix type + is(error[key], PartialJsonRpcErrorStruct.schema[key]) + ) { // @ts-expect-error TODO: Fix type acc[key] = error[key]; } return acc; }, {}); - return { ...fallbackError, ...subset }; + if (isNonEmptyArray(Object.keys(subset))) { + const message = getErrorMessage(error, fallbackError.message); + return { ...fallbackError, message, ...subset }; + } } - // TODO: If the original error is an object, we make a copy of it, this should also copy class properties to an object + // If the error is not a partial, use the fallback error, but try to include the original error as `cause` const cause = isObject(error) ? getCause(error) : error; const fallbackWithCause = { ...fallbackError, @@ -166,3 +174,20 @@ function getCause(error: T): Json { return acc; }, {}); } + +/** + * Extract a message from an error or use a fallback. + * + * @param error - The error class or object. + * @param fallbackMessage - The fallback message. + * @returns The error message. + */ +function getErrorMessage(error: T, fallbackMessage: string): string { + const originalMessage = + isObject(error) && typeof error.message === 'string' ? error.message : null; + const originalCode = + isObject(error) && typeof error.code === 'number' ? error.code : null; + const rpcCodeMessage = getMessageFromCode(originalCode, fallbackMessage); + + return originalMessage ?? rpcCodeMessage; +} From 5a145b0f9aa1f0cbce352137969d6d276923bae3 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 9 Mar 2023 11:06:19 +0100 Subject: [PATCH 09/15] Simplify error serialization strategy --- jest.config.js | 8 +- package.json | 5 +- src/utils.test.ts | 113 ++++++++-- src/utils.ts | 51 +---- yarn.lock | 526 ++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 616 insertions(+), 87 deletions(-) diff --git a/jest.config.js b/jest.config.js index 5dc0cac..32b85f5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,10 +41,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 92.3, - functions: 85, - lines: 96.04, - statements: 96.04, + branches: 93.58, + functions: 84.61, + lines: 96.71, + statements: 96.71, }, }, diff --git a/package.json b/package.json index 1e9db04..5b72176 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,8 @@ "test:watch": "jest --watch" }, "dependencies": { - "@metamask/utils": "^3.3.0", - "fast-safe-stringify": "^2.0.6", - "superstruct": "^0.16.7" + "@metamask/utils": "^5.0.0", + "fast-safe-stringify": "^2.0.6" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.0.3", diff --git a/src/utils.test.ts b/src/utils.test.ts index 2204a2b..b24dbb4 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -16,7 +16,7 @@ import { dummyData, } from './__fixtures__'; import { getMessageFromCode, serializeError } from './utils'; -import { errorCodes } from '.'; +import { errorCodes, ethErrors } from '.'; const rpcCodes = errorCodes.rpc; @@ -69,17 +69,28 @@ describe('serializeError', () => { it('handles invalid error: valid code, undefined message', () => { const result = serializeError(invalidError3); expect(result).toStrictEqual({ - code: 4001, - message: getMessageFromCode(4001), + code: errorCodes.rpc.internal, + message: getMessageFromCode(errorCodes.rpc.internal), + data: { + cause: { + code: 4001, + }, + }, }); }); it('handles invalid error: non-string message with data', () => { const result = serializeError(invalidError4); expect(result).toStrictEqual({ - code: 4001, - message: getMessageFromCode(4001), - data: Object.assign({}, dummyData), + code: rpcCodes.internal, + message: getMessageFromCode(rpcCodes.internal), + data: { + cause: { + code: invalidError4.code, + message: invalidError4.message, + data: Object.assign({}, dummyData), + }, + }, }); }); @@ -87,8 +98,14 @@ describe('serializeError', () => { const result = serializeError(invalidError7); expect(result).toStrictEqual({ code: rpcCodes.internal, - message: dummyMessage, - data: Object.assign({}, dummyData), + message: getMessageFromCode(rpcCodes.internal), + data: { + cause: { + code: invalidError7.code, + message: invalidError7.message, + data: Object.assign({}, dummyData), + }, + }, }); }); @@ -145,10 +162,8 @@ describe('serializeError', () => { }); }); - it('handles valid error: message, data, and stack', () => { - const result = serializeError( - Object.assign({}, validError1, { stack: 'foo' }), - ); + it('handles valid error: message and data', () => { + const result = serializeError(Object.assign({}, validError1)); expect(result).toStrictEqual({ code: 4001, message: validError1.message, @@ -157,7 +172,7 @@ describe('serializeError', () => { }); it('handles including stack: no stack present', () => { - const result = serializeError(validError1, { shouldIncludeStack: true }); + const result = serializeError(validError1); expect(result).toStrictEqual({ code: 4001, message: validError1.message, @@ -168,7 +183,6 @@ describe('serializeError', () => { it('handles including stack: string stack present', () => { const result = serializeError( Object.assign({}, validError1, { stack: 'foo' }), - { shouldIncludeStack: true }, ); expect(result).toStrictEqual({ code: 4001, @@ -178,12 +192,10 @@ describe('serializeError', () => { }); }); - it('handles including stack: non-string stack present', () => { + it('handles removing stack', () => { const result = serializeError( - Object.assign({}, validError1, { stack: 2 }), - { - shouldIncludeStack: true, - }, + Object.assign({}, validError1, { stack: 'foo' }), + { shouldIncludeStack: false }, ); expect(result).toStrictEqual({ code: 4001, @@ -193,10 +205,69 @@ describe('serializeError', () => { }); it('handles regular Error()', () => { - const result = serializeError(new Error('foo')); + const error = new Error('foo'); + const result = serializeError(error); expect(result).toStrictEqual({ code: errorCodes.rpc.internal, - message: 'foo', + message: getMessageFromCode(errorCodes.rpc.internal), + data: { + cause: { + message: error.message, + stack: error.stack, + }, + }, }); + + expect(JSON.parse(JSON.stringify(result))).toStrictEqual({ + code: errorCodes.rpc.internal, + message: getMessageFromCode(errorCodes.rpc.internal), + data: { + cause: { + message: error.message, + stack: error.stack, + }, + }, + }); + }); + + it('handles EthereumRpcError', () => { + const error = ethErrors.rpc.invalidParams(); + const result = serializeError(error); + expect(result).toStrictEqual({ + code: error.code, + message: error.message, + stack: error.stack, + }); + + expect(JSON.parse(JSON.stringify(result))).toStrictEqual({ + code: error.code, + message: error.message, + stack: error.stack, + }); + }); + + it('removes non JSON-serializable props on cause', () => { + const error = new Error('foo'); + // @ts-expect-error Intentionally using wrong type + error.message = () => undefined; + const result = serializeError(error); + expect(result).toStrictEqual({ + code: errorCodes.rpc.internal, + message: getMessageFromCode(errorCodes.rpc.internal), + data: { + cause: { + stack: error.stack, + }, + }, + }); + }); + + it('throws if fallback is invalid', () => { + expect(() => + // @ts-expect-error Intentionally using wrong type + serializeError(new Error(), { fallbackError: new Error() }), + ).toThrow( + 'Must provide fallback error with integer number code and string message.', + ); }); }); diff --git a/src/utils.ts b/src/utils.ts index f979aed..63da82d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,20 +1,17 @@ import { hasProperty, - JsonRpcErrorStruct, isValidJson, isObject, isJsonRpcError, Json, JsonRpcError, - isNonEmptyArray, } from '@metamask/utils'; -import { create, is, partial } from 'superstruct'; import { errorCodes, errorValues } from './error-constants'; import { EthereumRpcError } from './classes'; const FALLBACK_ERROR_CODE = errorCodes.rpc.internal; const FALLBACK_MESSAGE = - 'Invalid internal error. See "data.cause" for original value. Please report this bug.'; + 'Unspecified error message. This is a bug, please report it.'; const FALLBACK_ERROR: JsonRpcError = { code: FALLBACK_ERROR_CODE, message: getMessageFromCode(FALLBACK_ERROR_CODE), @@ -79,7 +76,7 @@ export function isValidCode(code: unknown): code is number { */ export function serializeError( error: unknown, - { fallbackError = FALLBACK_ERROR, shouldIncludeStack = false } = {}, + { fallbackError = FALLBACK_ERROR, shouldIncludeStack = true } = {}, ): JsonRpcError { if (!isJsonRpcError(fallbackError)) { throw new Error( @@ -100,10 +97,6 @@ export function serializeError( return serialized; } -const PartialJsonRpcErrorStruct = partial(JsonRpcErrorStruct); - -const ERROR_KEYS = Object.keys(JsonRpcErrorStruct.schema); - /** * Construct a JSON-serializable object given an error and a `fallbackError` * @@ -113,29 +106,10 @@ const ERROR_KEYS = Object.keys(JsonRpcErrorStruct.schema); */ function buildError(error: unknown, fallbackError: JsonRpcError): JsonRpcError { if (isJsonRpcError(error)) { - return create(error, JsonRpcErrorStruct); - } - - // If the original error is a partial JSON-RPC error, extract matching keys and merge with fallback error - if (isObject(error)) { - const subset = ERROR_KEYS.reduce((acc, key) => { - if ( - hasProperty(error, key) && - // @ts-expect-error TODO: Fix type - is(error[key], PartialJsonRpcErrorStruct.schema[key]) - ) { - // @ts-expect-error TODO: Fix type - acc[key] = error[key]; - } - return acc; - }, {}); - if (isNonEmptyArray(Object.keys(subset))) { - const message = getErrorMessage(error, fallbackError.message); - return { ...fallbackError, message, ...subset }; - } + return error; } - // If the error is not a partial, use the fallback error, but try to include the original error as `cause` + // If the error does not match the JsonRpcError type, use the fallback error, but try to include the original error as `cause` const cause = isObject(error) ? getCause(error) : error; const fallbackWithCause = { ...fallbackError, @@ -174,20 +148,3 @@ function getCause(error: T): Json { return acc; }, {}); } - -/** - * Extract a message from an error or use a fallback. - * - * @param error - The error class or object. - * @param fallbackMessage - The fallback message. - * @returns The error message. - */ -function getErrorMessage(error: T, fallbackMessage: string): string { - const originalMessage = - isObject(error) && typeof error.message === 'string' ? error.message : null; - const originalCode = - isObject(error) && typeof error.code === 'number' ? error.code : null; - const rpcCodeMessage = getMessageFromCode(originalCode, fallbackMessage); - - return originalMessage ?? rpcCodeMessage; -} diff --git a/yarn.lock b/yarn.lock index 48f2ed4..682f421 100644 --- a/yarn.lock +++ b/yarn.lock @@ -397,6 +397,33 @@ __metadata: languageName: node linkType: hard +"@chainsafe/as-sha256@npm:^0.3.1": + version: 0.3.1 + resolution: "@chainsafe/as-sha256@npm:0.3.1" + checksum: 58ea733be1657b0e31dbf48b0dba862da0833df34a81c1460c7352f04ce90874f70003cbf34d0afb9e5e53a33ee2d63a261a8b12462be85b2ba0a6f7f13d6150 + languageName: node + linkType: hard + +"@chainsafe/persistent-merkle-tree@npm:^0.4.2": + version: 0.4.2 + resolution: "@chainsafe/persistent-merkle-tree@npm:0.4.2" + dependencies: + "@chainsafe/as-sha256": ^0.3.1 + checksum: f9cfcb2132a243992709715dbd28186ab48c7c0c696f29d30857693cca5526bf753974a505ef68ffd5623bbdbcaa10f9083f4dd40bf99eb6408e451cc26a1a9e + languageName: node + linkType: hard + +"@chainsafe/ssz@npm:0.9.4": + version: 0.9.4 + resolution: "@chainsafe/ssz@npm:0.9.4" + dependencies: + "@chainsafe/as-sha256": ^0.3.1 + "@chainsafe/persistent-merkle-tree": ^0.4.2 + case: ^1.6.3 + checksum: c6eaedeae9e5618b3c666ff4507a27647f665a8dcf17d5ca86da4ed4788c5a93868f256d0005467d184fdf35ec03f323517ec2e55ec42492d769540a2ec396bc + languageName: node + linkType: hard + "@es-joy/jsdoccomment@npm:~0.33.4": version: 0.33.4 resolution: "@es-joy/jsdoccomment@npm:0.33.4" @@ -425,6 +452,310 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/common@npm:^3.1.1": + version: 3.1.1 + resolution: "@ethereumjs/common@npm:3.1.1" + dependencies: + "@ethereumjs/util": ^8.0.5 + crc-32: ^1.2.0 + checksum: 58602dee9fbcf691dca111b4fd7fd5770f5e86d68012ce48fba396c7038afdca4fca273a9cf39f88cf6ea7b256603a4bd214e94e9d01361efbcd060460b78952 + languageName: node + linkType: hard + +"@ethereumjs/rlp@npm:^4.0.1": + version: 4.0.1 + resolution: "@ethereumjs/rlp@npm:4.0.1" + bin: + rlp: bin/rlp + checksum: 30db19c78faa2b6ff27275ab767646929207bb207f903f09eb3e4c273ce2738b45f3c82169ddacd67468b4f063d8d96035f2bf36f02b6b7e4d928eefe2e3ecbc + languageName: node + linkType: hard + +"@ethereumjs/tx@npm:^4.1.1": + version: 4.1.1 + resolution: "@ethereumjs/tx@npm:4.1.1" + dependencies: + "@chainsafe/ssz": 0.9.4 + "@ethereumjs/common": ^3.1.1 + "@ethereumjs/rlp": ^4.0.1 + "@ethereumjs/util": ^8.0.5 + "@ethersproject/providers": ^5.7.2 + ethereum-cryptography: ^1.1.2 + peerDependencies: + c-kzg: ^1.0.8 + peerDependenciesMeta: + c-kzg: + optional: true + checksum: 98897e79adf03ee90ed98c6a543e15e0b4e127bc5bc381d70cdcc76b111574205b94869c29d925ea9e30a98e5ef8b0f5597914359deb9db552017b2e78ef17a8 + languageName: node + linkType: hard + +"@ethereumjs/util@npm:^8.0.5": + version: 8.0.5 + resolution: "@ethereumjs/util@npm:8.0.5" + dependencies: + "@chainsafe/ssz": 0.9.4 + "@ethereumjs/rlp": ^4.0.1 + ethereum-cryptography: ^1.1.2 + checksum: 318386785295b4584289b1aa576d2621392b3a918d127890db62d3f74184f3377694dd9e951e19bfb9ab80e8dc9e38e180236cac2651dead26097d10963731f9 + languageName: node + linkType: hard + +"@ethersproject/abstract-provider@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/abstract-provider@npm:5.7.0" + dependencies: + "@ethersproject/bignumber": ^5.7.0 + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + "@ethersproject/networks": ^5.7.0 + "@ethersproject/properties": ^5.7.0 + "@ethersproject/transactions": ^5.7.0 + "@ethersproject/web": ^5.7.0 + checksum: 74cf4696245cf03bb7cc5b6cbf7b4b89dd9a79a1c4688126d214153a938126d4972d42c93182198653ce1de35f2a2cad68be40337d4774b3698a39b28f0228a8 + languageName: node + linkType: hard + +"@ethersproject/abstract-signer@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/abstract-signer@npm:5.7.0" + dependencies: + "@ethersproject/abstract-provider": ^5.7.0 + "@ethersproject/bignumber": ^5.7.0 + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + "@ethersproject/properties": ^5.7.0 + checksum: a823dac9cfb761e009851050ebebd5b229d1b1cc4a75b125c2da130ff37e8218208f7f9d1386f77407705b889b23d4a230ad67185f8872f083143e0073cbfbe3 + languageName: node + linkType: hard + +"@ethersproject/address@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/address@npm:5.7.0" + dependencies: + "@ethersproject/bignumber": ^5.7.0 + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/keccak256": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + "@ethersproject/rlp": ^5.7.0 + checksum: 64ea5ebea9cc0e845c413e6cb1e54e157dd9fc0dffb98e239d3a3efc8177f2ff798cd4e3206cf3660ee8faeb7bef1a47dc0ebef0d7b132c32e61e550c7d4c843 + languageName: node + linkType: hard + +"@ethersproject/base64@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/base64@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + checksum: 7dd5d734d623582f08f665434f53685041a3d3b334a0e96c0c8afa8bbcaab934d50e5b6b980e826a8fde8d353e0b18f11e61faf17468177274b8e7c69cd9742b + languageName: node + linkType: hard + +"@ethersproject/basex@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/basex@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/properties": ^5.7.0 + checksum: 326087b7e1f3787b5fe6cd1cf2b4b5abfafbc355a45e88e22e5e9d6c845b613ffc5301d629b28d5c4d5e2bfe9ec424e6782c804956dff79be05f0098cb5817de + languageName: node + linkType: hard + +"@ethersproject/bignumber@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/bignumber@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + bn.js: ^5.2.1 + checksum: 8c9a134b76f3feb4ec26a5a27379efb4e156b8fb2de0678a67788a91c7f4e30abe9d948638458e4b20f2e42380da0adacc7c9389d05fce070692edc6ae9b4904 + languageName: node + linkType: hard + +"@ethersproject/bytes@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/bytes@npm:5.7.0" + dependencies: + "@ethersproject/logger": ^5.7.0 + checksum: 66ad365ceaab5da1b23b72225c71dce472cf37737af5118181fa8ab7447d696bea15ca22e3a0e8836fdd8cfac161afe321a7c67d0dde96f9f645ddd759676621 + languageName: node + linkType: hard + +"@ethersproject/constants@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/constants@npm:5.7.0" + dependencies: + "@ethersproject/bignumber": ^5.7.0 + checksum: 6d4b1355747cce837b3e76ec3bde70e4732736f23b04f196f706ebfa5d4d9c2be50904a390d4d40ce77803b98d03d16a9b6898418e04ba63491933ce08c4ba8a + languageName: node + linkType: hard + +"@ethersproject/hash@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/hash@npm:5.7.0" + dependencies: + "@ethersproject/abstract-signer": ^5.7.0 + "@ethersproject/address": ^5.7.0 + "@ethersproject/base64": ^5.7.0 + "@ethersproject/bignumber": ^5.7.0 + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/keccak256": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + "@ethersproject/properties": ^5.7.0 + "@ethersproject/strings": ^5.7.0 + checksum: 6e9fa8d14eb08171cd32f17f98cc108ec2aeca74a427655f0d689c550fee0b22a83b3b400fad7fb3f41cf14d4111f87f170aa7905bcbcd1173a55f21b06262ef + languageName: node + linkType: hard + +"@ethersproject/keccak256@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/keccak256@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + js-sha3: 0.8.0 + checksum: ff70950d82203aab29ccda2553422cbac2e7a0c15c986bd20a69b13606ed8bb6e4fdd7b67b8d3b27d4f841e8222cbaccd33ed34be29f866fec7308f96ed244c6 + languageName: node + linkType: hard + +"@ethersproject/logger@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/logger@npm:5.7.0" + checksum: 075ab2f605f1fd0813f2e39c3308f77b44a67732b36e712d9bc085f22a84aac4da4f71b39bee50fe78da3e1c812673fadc41180c9970fe5e486e91ea17befe0d + languageName: node + linkType: hard + +"@ethersproject/networks@npm:^5.7.0": + version: 5.7.1 + resolution: "@ethersproject/networks@npm:5.7.1" + dependencies: + "@ethersproject/logger": ^5.7.0 + checksum: 0339f312304c17d9a0adce550edb825d4d2c8c9468c1634c44172c67a9ed256f594da62c4cda5c3837a0f28b7fabc03aca9b492f68ff1fdad337ee861b27bd5d + languageName: node + linkType: hard + +"@ethersproject/properties@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/properties@npm:5.7.0" + dependencies: + "@ethersproject/logger": ^5.7.0 + checksum: 6ab0ccf0c3aadc9221e0cdc5306ce6cd0df7f89f77d77bccdd1277182c9ead0202cd7521329ba3acde130820bf8af299e17cf567d0d497c736ee918207bbf59f + languageName: node + linkType: hard + +"@ethersproject/providers@npm:^5.7.2": + version: 5.7.2 + resolution: "@ethersproject/providers@npm:5.7.2" + dependencies: + "@ethersproject/abstract-provider": ^5.7.0 + "@ethersproject/abstract-signer": ^5.7.0 + "@ethersproject/address": ^5.7.0 + "@ethersproject/base64": ^5.7.0 + "@ethersproject/basex": ^5.7.0 + "@ethersproject/bignumber": ^5.7.0 + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/constants": ^5.7.0 + "@ethersproject/hash": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + "@ethersproject/networks": ^5.7.0 + "@ethersproject/properties": ^5.7.0 + "@ethersproject/random": ^5.7.0 + "@ethersproject/rlp": ^5.7.0 + "@ethersproject/sha2": ^5.7.0 + "@ethersproject/strings": ^5.7.0 + "@ethersproject/transactions": ^5.7.0 + "@ethersproject/web": ^5.7.0 + bech32: 1.1.4 + ws: 7.4.6 + checksum: 1754c731a5ca6782ae9677f4a9cd8b6246c4ef21a966c9a01b133750f3c578431ec43ec254e699969c4a0f87e84463ded50f96b415600aabd37d2056aee58c19 + languageName: node + linkType: hard + +"@ethersproject/random@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/random@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + checksum: 017829c91cff6c76470852855108115b0b52c611b6be817ed1948d56ba42d6677803ec2012aa5ae298a7660024156a64c11fcf544e235e239ab3f89f0fff7345 + languageName: node + linkType: hard + +"@ethersproject/rlp@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/rlp@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + checksum: bce165b0f7e68e4d091c9d3cf47b247cac33252df77a095ca4281d32d5eeaaa3695d9bc06b2b057c5015353a68df89f13a4a54a72e888e4beeabbe56b15dda6e + languageName: node + linkType: hard + +"@ethersproject/sha2@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/sha2@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + hash.js: 1.1.7 + checksum: 09321057c022effbff4cc2d9b9558228690b5dd916329d75c4b1ffe32ba3d24b480a367a7cc92d0f0c0b1c896814d03351ae4630e2f1f7160be2bcfbde435dbc + languageName: node + linkType: hard + +"@ethersproject/signing-key@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/signing-key@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + "@ethersproject/properties": ^5.7.0 + bn.js: ^5.2.1 + elliptic: 6.5.4 + hash.js: 1.1.7 + checksum: 8f8de09b0aac709683bbb49339bc0a4cd2f95598f3546436c65d6f3c3a847ffa98e06d35e9ed2b17d8030bd2f02db9b7bd2e11c5cf8a71aad4537487ab4cf03a + languageName: node + linkType: hard + +"@ethersproject/strings@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/strings@npm:5.7.0" + dependencies: + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/constants": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + checksum: 5ff78693ae3fdf3cf23e1f6dc047a61e44c8197d2408c42719fef8cb7b7b3613a4eec88ac0ed1f9f5558c74fe0de7ae3195a29ca91a239c74b9f444d8e8b50df + languageName: node + linkType: hard + +"@ethersproject/transactions@npm:^5.7.0": + version: 5.7.0 + resolution: "@ethersproject/transactions@npm:5.7.0" + dependencies: + "@ethersproject/address": ^5.7.0 + "@ethersproject/bignumber": ^5.7.0 + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/constants": ^5.7.0 + "@ethersproject/keccak256": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + "@ethersproject/properties": ^5.7.0 + "@ethersproject/rlp": ^5.7.0 + "@ethersproject/signing-key": ^5.7.0 + checksum: a31b71996d2b283f68486241bff0d3ea3f1ba0e8f1322a8fffc239ccc4f4a7eb2ea9994b8fd2f093283fd75f87bae68171e01b6265261f821369aca319884a79 + languageName: node + linkType: hard + +"@ethersproject/web@npm:^5.7.0": + version: 5.7.1 + resolution: "@ethersproject/web@npm:5.7.1" + dependencies: + "@ethersproject/base64": ^5.7.0 + "@ethersproject/bytes": ^5.7.0 + "@ethersproject/logger": ^5.7.0 + "@ethersproject/properties": ^5.7.0 + "@ethersproject/strings": ^5.7.0 + checksum: 7028c47103f82fd2e2c197ce0eecfacaa9180ffeec7de7845b1f4f9b19d84081b7a48227aaddde05a4aaa526af574a9a0ce01cc0fc75e3e371f84b38b5b16b2b + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -894,7 +1225,7 @@ __metadata: "@metamask/eslint-config-jest": ^10.0.0 "@metamask/eslint-config-nodejs": ^10.0.0 "@metamask/eslint-config-typescript": ^10.0.0 - "@metamask/utils": ^3.3.0 + "@metamask/utils": ^5.0.0 "@types/jest": ^28.1.6 "@typescript-eslint/eslint-plugin": ^5.33.1 "@typescript-eslint/parser": ^5.33.1 @@ -911,21 +1242,36 @@ __metadata: jest-it-up: ^2.0.2 prettier: ^2.7.1 prettier-plugin-packagejson: ^2.2.18 - superstruct: ^0.16.7 ts-jest: ^28.0.7 typedoc: ^0.23.19 typescript: ~4.7.4 languageName: unknown linkType: soft -"@metamask/utils@npm:^3.3.0": - version: 3.3.0 - resolution: "@metamask/utils@npm:3.3.0" +"@metamask/utils@npm:^5.0.0": + version: 5.0.0 + resolution: "@metamask/utils@npm:5.0.0" dependencies: + "@ethereumjs/tx": ^4.1.1 "@types/debug": ^4.1.7 debug: ^4.3.4 - superstruct: ^0.16.7 - checksum: 263c26722b7339fced997cc2aa38ccb34d178f532ff025c7e789818ad5ef2317439eda694a4c0bf5ff3cabdeced3c170b9ad23d2f178ddd5fd225f6ec2bce259 + semver: ^7.3.8 + superstruct: ^1.0.3 + checksum: 34e39fc0bf28db5fe92676753de3291b05a517f8c81dbe332a4b6002739a58450a89fb2bddd85922a4f420affb1674604e6ad4627cdf052459e5371361ef7dd2 + languageName: node + linkType: hard + +"@noble/hashes@npm:1.2.0, @noble/hashes@npm:~1.2.0": + version: 1.2.0 + resolution: "@noble/hashes@npm:1.2.0" + checksum: 8ca080ce557b8f40fb2f78d3aedffd95825a415ac8e13d7ffe3643f8626a8c2d99a3e5975b555027ac24316d8b3c02a35b8358567c0c23af681e6573602aa434 + languageName: node + linkType: hard + +"@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:~1.7.0": + version: 1.7.1 + resolution: "@noble/secp256k1@npm:1.7.1" + checksum: d2301f1f7690368d8409a3152450458f27e54df47e3f917292de3de82c298770890c2de7c967d237eff9c95b70af485389a9695f73eb05a43e2bd562d18b18cb languageName: node linkType: hard @@ -1004,6 +1350,34 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:~1.1.0": + version: 1.1.1 + resolution: "@scure/base@npm:1.1.1" + checksum: b4fc810b492693e7e8d0107313ac74c3646970c198bbe26d7332820886fa4f09441991023ec9aa3a2a51246b74409ab5ebae2e8ef148bbc253da79ac49130309 + languageName: node + linkType: hard + +"@scure/bip32@npm:1.1.5": + version: 1.1.5 + resolution: "@scure/bip32@npm:1.1.5" + dependencies: + "@noble/hashes": ~1.2.0 + "@noble/secp256k1": ~1.7.0 + "@scure/base": ~1.1.0 + checksum: b08494ab0d2b1efee7226d1b5100db5157ebea22a78bb87126982a76a186cb3048413e8be0ba2622d00d048a20acbba527af730de86c132a77de616eb9907a3b + languageName: node + linkType: hard + +"@scure/bip39@npm:1.1.1": + version: 1.1.1 + resolution: "@scure/bip39@npm:1.1.1" + dependencies: + "@noble/hashes": ~1.2.0 + "@scure/base": ~1.1.0 + checksum: fbb594c50696fa9c14e891d872f382e50a3f919b6c96c55ef2fb10c7102c546dafb8f099a62bd114c12a00525b595dcf7381846f383f0ddcedeaa6e210747d2f + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.24.1": version: 0.24.51 resolution: "@sinclair/typebox@npm:0.24.51" @@ -1675,6 +2049,27 @@ __metadata: languageName: node linkType: hard +"bech32@npm:1.1.4": + version: 1.1.4 + resolution: "bech32@npm:1.1.4" + checksum: 0e98db619191548390d6f09ff68b0253ba7ae6a55db93dfdbb070ba234c1fd3308c0606fbcc95fad50437227b10011e2698b89f0181f6e7f845c499bd14d0f4b + languageName: node + linkType: hard + +"bn.js@npm:^4.11.9": + version: 4.12.0 + resolution: "bn.js@npm:4.12.0" + checksum: 39afb4f15f4ea537b55eaf1446c896af28ac948fdcf47171961475724d1bb65118cca49fa6e3d67706e4790955ec0e74de584e45c8f1ef89f46c812bee5b5a12 + languageName: node + linkType: hard + +"bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: 3dd8c8d38055fedfa95c1d5fc3c99f8dd547b36287b37768db0abab3c239711f88ff58d18d155dd8ad902b0b0cee973747b7ae20ea12a09473272b0201c9edd3 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -1703,6 +2098,13 @@ __metadata: languageName: node linkType: hard +"brorand@npm:^1.1.0": + version: 1.1.0 + resolution: "brorand@npm:1.1.0" + checksum: 8a05c9f3c4b46572dec6ef71012b1946db6cae8c7bb60ccd4b7dd5a84655db49fe043ecc6272e7ef1f69dc53d6730b9e2a3a03a8310509a3d797a618cbee52be + languageName: node + linkType: hard + "browserslist@npm:^4.21.3": version: 4.21.4 resolution: "browserslist@npm:4.21.4" @@ -1806,6 +2208,13 @@ __metadata: languageName: node linkType: hard +"case@npm:^1.6.3": + version: 1.6.3 + resolution: "case@npm:1.6.3" + checksum: febe73278f910b0d28aab7efd6f51c235f9aa9e296148edb56dfb83fd58faa88308c30ce9a0122b6e53e0362c44f4407105bd5ef89c46860fc2b184e540fd68d + languageName: node + linkType: hard + "caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -2025,6 +2434,15 @@ __metadata: languageName: node linkType: hard +"crc-32@npm:^1.2.0": + version: 1.2.2 + resolution: "crc-32@npm:1.2.2" + bin: + crc32: bin/crc32.njs + checksum: ad2d0ad0cbd465b75dcaeeff0600f8195b686816ab5f3ba4c6e052a07f728c3e70df2e3ca9fd3d4484dc4ba70586e161ca5a2334ec8bf5a41bf022a6103ff243 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -2199,6 +2617,21 @@ __metadata: languageName: node linkType: hard +"elliptic@npm:6.5.4": + version: 6.5.4 + resolution: "elliptic@npm:6.5.4" + dependencies: + bn.js: ^4.11.9 + brorand: ^1.1.0 + hash.js: ^1.0.0 + hmac-drbg: ^1.0.1 + inherits: ^2.0.4 + minimalistic-assert: ^1.0.1 + minimalistic-crypto-utils: ^1.0.1 + checksum: d56d21fd04e97869f7ffcc92e18903b9f67f2d4637a23c860492fbbff5a3155fd9ca0184ce0c865dd6eb2487d234ce9551335c021c376cd2d3b7cb749c7d10f4 + languageName: node + linkType: hard + "emittery@npm:^0.10.2": version: 0.10.2 resolution: "emittery@npm:0.10.2" @@ -2628,6 +3061,18 @@ __metadata: languageName: node linkType: hard +"ethereum-cryptography@npm:^1.1.2": + version: 1.2.0 + resolution: "ethereum-cryptography@npm:1.2.0" + dependencies: + "@noble/hashes": 1.2.0 + "@noble/secp256k1": 1.7.1 + "@scure/bip32": 1.1.5 + "@scure/bip39": 1.1.1 + checksum: 97e8e8253cb9f5a9271bd0201c37609c451c890eb85883b9c564f14743c3d7c673287406c93bf5604307593ee298ad9a03983388b85c11ca61461b9fc1a4f2c7 + languageName: node + linkType: hard + "execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -3167,6 +3612,27 @@ __metadata: languageName: node linkType: hard +"hash.js@npm:1.1.7, hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": + version: 1.1.7 + resolution: "hash.js@npm:1.1.7" + dependencies: + inherits: ^2.0.3 + minimalistic-assert: ^1.0.1 + checksum: e350096e659c62422b85fa508e4b3669017311aa4c49b74f19f8e1bc7f3a54a584fdfd45326d4964d6011f2b2d882e38bea775a96046f2a61b7779a979629d8f + languageName: node + linkType: hard + +"hmac-drbg@npm:^1.0.1": + version: 1.0.1 + resolution: "hmac-drbg@npm:1.0.1" + dependencies: + hash.js: ^1.0.3 + minimalistic-assert: ^1.0.0 + minimalistic-crypto-utils: ^1.0.1 + checksum: bd30b6a68d7f22d63f10e1888aee497d7c2c5c0bb469e66bbdac99f143904d1dfe95f8131f95b3e86c86dd239963c9d972fcbe147e7cffa00e55d18585c43fe0 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -3298,7 +3764,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -4047,6 +4513,13 @@ __metadata: languageName: node linkType: hard +"js-sha3@npm:0.8.0": + version: 0.8.0 + resolution: "js-sha3@npm:0.8.0" + checksum: 75df77c1fc266973f06cce8309ce010e9e9f07ec35ab12022ed29b7f0d9c8757f5a73e1b35aa24840dced0dea7059085aa143d817aea9e188e2a80d569d9adce + languageName: node + linkType: hard + "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -4372,6 +4845,20 @@ __metadata: languageName: node linkType: hard +"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-assert@npm:1.0.1" + checksum: cc7974a9268fbf130fb055aff76700d7e2d8be5f761fb5c60318d0ed010d839ab3661a533ad29a5d37653133385204c503bfac995aaa4236f4e847461ea32ba7 + languageName: node + linkType: hard + +"minimalistic-crypto-utils@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-crypto-utils@npm:1.0.1" + checksum: 6e8a0422b30039406efd4c440829ea8f988845db02a3299f372fceba56ffa94994a9c0f2fd70c17f9969eedfbd72f34b5070ead9656a34d3f71c0bd72583a0ed + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -5554,10 +6041,10 @@ __metadata: languageName: node linkType: hard -"superstruct@npm:^0.16.7": - version: 0.16.7 - resolution: "superstruct@npm:0.16.7" - checksum: c8c855ff6945da8a41048c6d236de7b1af5d4d9c31742b3ee54d65647c31597488620281f65e095d5efc9e2fbdaad529b8c8f2506c12569d428467c835a21477 +"superstruct@npm:^1.0.3": + version: 1.0.3 + resolution: "superstruct@npm:1.0.3" + checksum: 761790bb111e6e21ddd608299c252f3be35df543263a7ebbc004e840d01fcf8046794c274bcb351bdf3eae4600f79d317d085cdbb19ca05803a4361840cc9bb1 languageName: node linkType: hard @@ -6007,6 +6494,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:7.4.6": + version: 7.4.6 + resolution: "ws@npm:7.4.6" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 3a990b32ed08c72070d5e8913e14dfcd831919205be52a3ff0b4cdd998c8d554f167c9df3841605cde8b11d607768cacab3e823c58c96a5c08c987e093eb767a + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" From 7c0a984e0761559b59ec50f4cc6638dc1bd22f33 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 9 Mar 2023 11:16:04 +0100 Subject: [PATCH 10/15] Simplify --- src/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 63da82d..2cbe704 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -84,10 +84,6 @@ export function serializeError( ); } - if (error instanceof EthereumRpcError) { - return error.serialize(); - } - const serialized = buildError(error, fallbackError as JsonRpcError); if (!shouldIncludeStack) { @@ -105,6 +101,10 @@ export function serializeError( * @returns A JSON serializable error object. */ function buildError(error: unknown, fallbackError: JsonRpcError): JsonRpcError { + if (error instanceof EthereumRpcError) { + return error.serialize(); + } + if (isJsonRpcError(error)) { return error; } From 2b337bc45b04d2c55d5d1c3568254d26ff9e6600 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 10 Mar 2023 11:12:20 +0100 Subject: [PATCH 11/15] Fix PR comments --- jest.config.js | 8 ++++---- src/utils.test.ts | 22 +++++++++++++++++++++- src/utils.ts | 46 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/jest.config.js b/jest.config.js index 32b85f5..e8fa9e1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,10 +41,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.58, - functions: 84.61, - lines: 96.71, - statements: 96.71, + branches: 94.31, + functions: 85, + lines: 96.85, + statements: 96.85, }, }, diff --git a/src/utils.test.ts b/src/utils.test.ts index b24dbb4..457fa79 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -44,7 +44,7 @@ describe('serializeError', () => { expect(result).toStrictEqual({ code: rpcCodes.internal, message: getMessageFromCode(rpcCodes.internal), - data: { cause: invalidError6 }, + data: { cause: null }, }); }); @@ -270,4 +270,24 @@ describe('serializeError', () => { 'Must provide fallback error with integer number code and string message.', ); }); + + it('handles arrays passed as error', () => { + const error = ['foo', Symbol('bar'), { baz: 'qux', symbol: Symbol('') }]; + const result = serializeError(error); + expect(result).toStrictEqual({ + code: rpcCodes.internal, + message: getMessageFromCode(rpcCodes.internal), + data: { + cause: ['foo', null, { baz: 'qux' }], + }, + }); + + expect(JSON.parse(JSON.stringify(result))).toStrictEqual({ + code: rpcCodes.internal, + message: getMessageFromCode(rpcCodes.internal), + data: { + cause: ['foo', null, { baz: 'qux' }], + }, + }); + }); }); diff --git a/src/utils.ts b/src/utils.ts index 2cbe704..58f8c17 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,7 @@ import { isJsonRpcError, Json, JsonRpcError, + RuntimeObject, } from '@metamask/utils'; import { errorCodes, errorValues } from './error-constants'; import { EthereumRpcError } from './classes'; @@ -84,7 +85,7 @@ export function serializeError( ); } - const serialized = buildError(error, fallbackError as JsonRpcError); + const serialized = buildError(error, fallbackError); if (!shouldIncludeStack) { delete serialized.stack; @@ -110,13 +111,13 @@ function buildError(error: unknown, fallbackError: JsonRpcError): JsonRpcError { } // If the error does not match the JsonRpcError type, use the fallback error, but try to include the original error as `cause` - const cause = isObject(error) ? getCause(error) : error; + const cause = serializeCause(error); const fallbackWithCause = { ...fallbackError, data: { cause }, }; - return fallbackWithCause as JsonRpcError; + return fallbackWithCause; } /** @@ -130,14 +131,41 @@ function isJsonRpcServerError(code: number): boolean { } /** - * Get cause from an object or a class, extracting all properties. + * Serializes an unknown error to be used as the `cause` in a fallback error. * - * @param error - The error class or object. - * @returns An object containing all the error properties. + * @param error - The unknown error. + * @returns A JSON-serializable object containing as much information about the original error as possible. */ -function getCause(error: T): Json { - return Object.getOwnPropertyNames(error).reduce((acc, key) => { - const value = error[key as keyof T]; +function serializeCause(error: unknown): Json { + if (Array.isArray(error)) { + return error.map((entry) => { + if (isValidJson(entry)) { + return entry; + } else if (isObject(entry)) { + return serializeObject(entry); + } + return null; + }); + } else if (isObject(error)) { + return serializeObject(error); + } + + if (isValidJson(error)) { + return error; + } + + return null; +} + +/** + * Extracts all JSON-serializable properties from an object. + * + * @param object - The object in question. + * @returns An object containing all the JSON-serializable properties. + */ +function serializeObject(object: RuntimeObject): Json { + return Object.getOwnPropertyNames(object).reduce((acc, key) => { + const value = object[key]; if (isValidJson(value)) { return { ...acc, From 8abb62b0af31124269742479c0297fc18ff6c1bc Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 13 Mar 2023 11:20:15 +0100 Subject: [PATCH 12/15] Simplify further --- jest.config.js | 2 +- src/utils.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/jest.config.js b/jest.config.js index e8fa9e1..8ba42da 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,7 +41,7 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 94.31, + branches: 94.25, functions: 85, lines: 96.85, statements: 96.85, diff --git a/src/utils.ts b/src/utils.ts index 58f8c17..40c97b6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -164,15 +164,15 @@ function serializeCause(error: unknown): Json { * @returns An object containing all the JSON-serializable properties. */ function serializeObject(object: RuntimeObject): Json { - return Object.getOwnPropertyNames(object).reduce((acc, key) => { - const value = object[key]; - if (isValidJson(value)) { - return { - ...acc, - [key]: value, - }; - } + return Object.getOwnPropertyNames(object).reduce>( + (acc, key) => { + const value = object[key]; + if (isValidJson(value)) { + acc[key] = value; + } - return acc; - }, {}); + return acc; + }, + {}, + ); } From 9ddba1e1817069001b7ff3001ee7c4d79e206e68 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 11 Apr 2023 14:20:19 +0200 Subject: [PATCH 13/15] Clarify doc string --- src/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 40c97b6..a86153b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -70,7 +70,7 @@ export function isValidCode(code: unknown): code is number { * @param error - The error to serialize. * @param options - Options bag. * @param options.fallbackError - The error to return if the given error is - * not compatible. + * not compatible. Should be a JSON serializable value. * @param options.shouldIncludeStack - Whether to include the error's stack * on the returned object. * @returns The serialized error. @@ -95,10 +95,10 @@ export function serializeError( } /** - * Construct a JSON-serializable object given an error and a `fallbackError` + * Construct a JSON-serializable object given an error and a JSON serializable `fallbackError` * * @param error - The error in question. - * @param fallbackError - The fallback error. + * @param fallbackError - A JSON serializable fallback error. * @returns A JSON serializable error object. */ function buildError(error: unknown, fallbackError: JsonRpcError): JsonRpcError { From 07fe7d475fcd682d13f58717307d3a10335ef149 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 11 Apr 2023 14:49:22 +0200 Subject: [PATCH 14/15] Address PR comments --- jest.config.js | 6 +++--- package.json | 2 +- src/utils.ts | 12 ++++++++---- yarn.lock | 18 +++++++++--------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/jest.config.js b/jest.config.js index 8ba42da..b456054 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,10 +41,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 94.25, + branches: 94.44, functions: 85, - lines: 96.85, - statements: 96.85, + lines: 96.87, + statements: 96.87, }, }, diff --git a/package.json b/package.json index 5b72176..5ce51c5 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "prettier-plugin-packagejson": "^2.2.18", "ts-jest": "^28.0.7", "typedoc": "^0.23.19", - "typescript": "~4.7.4" + "typescript": "~4.9.5" }, "packageManager": "yarn@3.2.4", "engines": { diff --git a/src/utils.ts b/src/utils.ts index a86153b..1c27119 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,7 +8,6 @@ import { RuntimeObject, } from '@metamask/utils'; import { errorCodes, errorValues } from './error-constants'; -import { EthereumRpcError } from './classes'; const FALLBACK_ERROR_CODE = errorCodes.rpc.internal; const FALLBACK_MESSAGE = @@ -63,7 +62,6 @@ export function isValidCode(code: unknown): code is number { /** * Serializes the given error to an Ethereum JSON RPC-compatible error object. - * Merely copies the given error's values if it is already compatible. * If the given error is not fully compatible, it will be preserved on the * returned object's data.cause property. * @@ -102,7 +100,13 @@ export function serializeError( * @returns A JSON serializable error object. */ function buildError(error: unknown, fallbackError: JsonRpcError): JsonRpcError { - if (error instanceof EthereumRpcError) { + // If an error specifies a `serialize` function, we call it and return the result. + if ( + error && + typeof error === 'object' && + 'serialize' in error && + typeof error.serialize === 'function' + ) { return error.serialize(); } @@ -110,7 +114,7 @@ function buildError(error: unknown, fallbackError: JsonRpcError): JsonRpcError { return error; } - // If the error does not match the JsonRpcError type, use the fallback error, but try to include the original error as `cause` + // If the error does not match the JsonRpcError type, use the fallback error, but try to include the original error as `cause`. const cause = serializeCause(error); const fallbackWithCause = { ...fallbackError, diff --git a/yarn.lock b/yarn.lock index 682f421..a81a952 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1244,7 +1244,7 @@ __metadata: prettier-plugin-packagejson: ^2.2.18 ts-jest: ^28.0.7 typedoc: ^0.23.19 - typescript: ~4.7.4 + typescript: ~4.9.5 languageName: unknown linkType: soft @@ -6292,23 +6292,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~4.7.4": - version: 4.7.4 - resolution: "typescript@npm:4.7.4" +"typescript@npm:~4.9.5": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 5750181b1cd7e6482c4195825547e70f944114fb47e58e4aa7553e62f11b3f3173766aef9c281783edfd881f7b8299cf35e3ca8caebe73d8464528c907a164df + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db languageName: node linkType: hard -"typescript@patch:typescript@~4.7.4#~builtin": - version: 4.7.4 - resolution: "typescript@patch:typescript@npm%3A4.7.4#~builtin::version=4.7.4&hash=701156" +"typescript@patch:typescript@~4.9.5#~builtin": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=701156" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 9096d8f6c16cb80ef3bf96fcbbd055bf1c4a43bd14f3b7be45a9fbe7ada46ec977f604d5feed3263b4f2aa7d4c7477ce5f9cd87de0d6feedec69a983f3a4f93e + checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 languageName: node linkType: hard From 5349014a6ad6173be18ddec648ec5f5d597dd1e4 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 11 Apr 2023 14:56:05 +0200 Subject: [PATCH 15/15] Add another test case --- src/utils.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/utils.test.ts b/src/utils.test.ts index 457fa79..b73801d 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -246,6 +246,25 @@ describe('serializeError', () => { }); }); + it('handles class that has serialize function', () => { + class MockClass { + serialize() { + return { code: 1, message: 'foo' }; + } + } + const error = new MockClass(); + const result = serializeError(error); + expect(result).toStrictEqual({ + code: 1, + message: 'foo', + }); + + expect(JSON.parse(JSON.stringify(result))).toStrictEqual({ + code: 1, + message: 'foo', + }); + }); + it('removes non JSON-serializable props on cause', () => { const error = new Error('foo'); // @ts-expect-error Intentionally using wrong type