From 6013edf6681ffb057f0788d86b6b0b4b4103f810 Mon Sep 17 00:00:00 2001 From: legobt <6wbvkn0j@anonaddy.me> Date: Mon, 13 May 2024 00:05:04 +0000 Subject: [PATCH 1/5] feat: propagate data.cause as cause in JsonRpcError constructor --- jest.config.js | 6 +++--- src/classes.ts | 12 +++++++++++- tsconfig.json | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/jest.config.js b/jest.config.js index b5b7788..5a7f9f2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -45,10 +45,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 92.64, + branches: 93.05, functions: 94.44, - lines: 92.85, - statements: 92.85, + lines: 92.96, + statements: 92.96, }, }, diff --git a/src/classes.ts b/src/classes.ts index 624dd8f..e013a8b 100644 --- a/src/classes.ts +++ b/src/classes.ts @@ -2,7 +2,7 @@ import type { Json, JsonRpcError as SerializedJsonRpcError, } from '@metamask/utils'; -import { isPlainObject } from '@metamask/utils'; +import { hasProperty, isObject, isPlainObject } from '@metamask/utils'; import safeStringify from 'fast-safe-stringify'; import type { OptionalDataWithOptionalCause } from './utils'; @@ -19,6 +19,9 @@ export type { SerializedJsonRpcError }; export class JsonRpcError< Data extends OptionalDataWithOptionalCause, > extends Error { + // This can be removed when tsconfig lib and/or target have changed to >=es2022 + public cause: OptionalDataWithOptionalCause; + public code: number; public data?: Data; @@ -36,6 +39,13 @@ export class JsonRpcError< this.code = code; if (data !== undefined) { this.data = data; + if ( + isObject(data) && + hasProperty(data, 'cause') && + isObject(data.cause) + ) { + this.cause = data.cause; + } } } diff --git a/tsconfig.json b/tsconfig.json index 56f6531..b49529a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, + // Remove custom `cause` field from JsonRpcError when updating "lib": ["ES2020"], "module": "CommonJS", "moduleResolution": "node", From e6350af139363c13f0d515f66a5e143011a6538c Mon Sep 17 00:00:00 2001 From: legobt <6wbvkn0j@anonaddy.me> Date: Mon, 13 May 2024 00:05:28 +0000 Subject: [PATCH 2/5] test: add cases for cause propagation --- src/__fixtures__/errors.ts | 6 +++++- src/errors.test.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/__fixtures__/errors.ts b/src/__fixtures__/errors.ts index 13cbb76..78e950b 100644 --- a/src/__fixtures__/errors.ts +++ b/src/__fixtures__/errors.ts @@ -1,7 +1,11 @@ import { rpcErrors } from '..'; -export const dummyData = { foo: 'bar' }; export const dummyMessage = 'baz'; +export const dummyData = { foo: 'bar' }; +export const dummyDataWithCause = { + foo: 'bar', + cause: { message: dummyMessage }, +}; export const invalidError0 = 0; export const invalidError1 = ['foo', 'bar', 3]; diff --git a/src/errors.test.ts b/src/errors.test.ts index 9e6a24f..af3caad 100644 --- a/src/errors.test.ts +++ b/src/errors.test.ts @@ -3,6 +3,8 @@ import { assert, isPlainObject } from '@metamask/utils'; import { rpcErrors, providerErrors, errorCodes } from '.'; import { dummyData, + dummyDataWithCause, + dummyMessage, CUSTOM_ERROR_MESSAGE, SERVER_ERROR_CODE, CUSTOM_ERROR_CODE, @@ -97,6 +99,21 @@ describe('rpcErrors', () => { }, ); + it.each(Object.entries(rpcErrors).filter(([key]) => key !== 'server'))( + '%s propagates data.cause if set', + (key, value) => { + const createError = value as any; + const error = createError({ + message: null, + data: Object.assign({}, dummyDataWithCause), + }); + // @ts-expect-error TypeScript does not like indexing into this with the key + const rpcCode = errorCodes.rpc[key]; + expect(error.message).toBe(getMessageFromCode(rpcCode)); + expect(error.cause.message).toBe(dummyMessage); + }, + ); + it('serializes a cause', () => { const error = rpcErrors.invalidInput({ data: { @@ -156,6 +173,21 @@ describe('providerErrors', () => { }, ); + it.each(Object.entries(providerErrors).filter(([key]) => key !== 'custom'))( + '%s propagates data.cause if set', + (key, value) => { + const createError = value as any; + const error = createError({ + message: null, + data: Object.assign({}, dummyDataWithCause), + }); + // @ts-expect-error TypeScript does not like indexing into this with the key + const providerCode = errorCodes.provider[key]; + expect(error.message).toBe(getMessageFromCode(providerCode)); + expect(error.cause.message).toBe(dummyMessage); + }, + ); + it('custom returns appropriate value', () => { const error = providerErrors.custom({ code: CUSTOM_ERROR_CODE, From 09d433ad219e0e901448f6b249b51833c9865dba Mon Sep 17 00:00:00 2001 From: legobt <6wbvkn0j@anonaddy.me> Date: Fri, 31 May 2024 01:26:53 +0000 Subject: [PATCH 3/5] improve cause-handling - break out util function dataHasCause with test - comment clarity - use native causes when available note: jest coverage depends on runtime --- jest.config.js | 8 ++++---- src/classes.ts | 31 ++++++++++++++++++------------- src/index.ts | 7 ++++++- src/utils.test.ts | 22 +++++++++++++++++++++- src/utils.ts | 13 +++++++++++++ tsconfig.json | 2 +- 6 files changed, 63 insertions(+), 20 deletions(-) diff --git a/jest.config.js b/jest.config.js index 5a7f9f2..dabcda1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -45,10 +45,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.05, - functions: 94.44, - lines: 92.96, - statements: 92.96, + branches: 91.89, + functions: 94.59, + lines: 92.42, + statements: 92.42, }, }, diff --git a/src/classes.ts b/src/classes.ts index e013a8b..d95f4b5 100644 --- a/src/classes.ts +++ b/src/classes.ts @@ -2,11 +2,11 @@ import type { Json, JsonRpcError as SerializedJsonRpcError, } from '@metamask/utils'; -import { hasProperty, isObject, isPlainObject } from '@metamask/utils'; +import { isPlainObject } from '@metamask/utils'; import safeStringify from 'fast-safe-stringify'; import type { OptionalDataWithOptionalCause } from './utils'; -import { serializeCause } from './utils'; +import { dataHasCause, serializeCause } from './utils'; export type { SerializedJsonRpcError }; @@ -19,8 +19,8 @@ export type { SerializedJsonRpcError }; export class JsonRpcError< Data extends OptionalDataWithOptionalCause, > extends Error { - // This can be removed when tsconfig lib and/or target have changed to >=es2022 - public cause: OptionalDataWithOptionalCause; + // The `cause` field can be removed when tsconfig lib and/or target have changed to >=es2022 + public cause?: unknown; public code: number; @@ -35,18 +35,23 @@ export class JsonRpcError< throw new Error('"message" must be a non-empty string.'); } - super(message); - this.code = code; + if (dataHasCause(data)) { + // @ts-expect-error - Error class does accept options argument depending on runtime, but types are mapping to oldest supported + super(message, { cause: data.cause }); + + // Browser backwards-compatibility fallback + if (!Object.prototype.hasOwnProperty.call(this, 'cause')) { + Object.assign(this, { cause: data.cause }); + } + } else { + super(message); + } + if (data !== undefined) { this.data = data; - if ( - isObject(data) && - hasProperty(data, 'cause') && - isObject(data.cause) - ) { - this.cause = data.cause; - } } + + this.code = code; } /** diff --git a/src/index.ts b/src/index.ts index f27e10d..120afee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,10 @@ export { JsonRpcError, EthereumProviderError } from './classes'; -export { serializeCause, serializeError, getMessageFromCode } from './utils'; +export { + dataHasCause, + serializeCause, + serializeError, + getMessageFromCode, +} from './utils'; export type { DataWithOptionalCause, OptionalDataWithOptionalCause, diff --git a/src/utils.test.ts b/src/utils.test.ts index 7529668..9aefd4a 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -16,7 +16,7 @@ import { dummyMessage, dummyData, } from './__fixtures__'; -import { getMessageFromCode, serializeError } from './utils'; +import { dataHasCause, getMessageFromCode, serializeError } from './utils'; const rpcCodes = errorCodes.rpc; @@ -310,3 +310,23 @@ describe('serializeError', () => { }); }); }); + +describe('dataHasCause', () => { + it('returns false for invalid data types', () => { + [undefined, null, 'hello', 1234].forEach((data) => { + const result = dataHasCause(data); + expect(result).toBe(false); + }); + }); + it('returns false for invalid cause types', () => { + [undefined, null, 'hello', 1234].forEach((cause) => { + const result = dataHasCause({ cause }); + expect(result).toBe(false); + }); + }); + it('returns true when cause is object', () => { + const data = { cause: {} }; + const result = dataHasCause(data); + expect(result).toBe(true); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index e3328a7..de3e061 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -211,3 +211,16 @@ function serializeObject(object: RuntimeObject): Json { {}, ); } + +/** + * Returns true if supplied error data has a usable `cause` property; false otherwise. + * + * @param data - Optional data to validate. + * @returns Whether cause property is present and an object. + */ +export function dataHasCause(data: unknown): data is { + [key: string]: Json | unknown; + cause: object; +} { + return isObject(data) && hasProperty(data, 'cause') && isObject(data.cause); +} diff --git a/tsconfig.json b/tsconfig.json index b49529a..e5b85d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, - // Remove custom `cause` field from JsonRpcError when updating + // Reminder: Remove custom `cause` field from JsonRpcError when enabling es2022 "lib": ["ES2020"], "module": "CommonJS", "moduleResolution": "node", From 99ff7f6322d384cbc59173f5f3998d1ea6664a2d Mon Sep 17 00:00:00 2001 From: legobt <6wbvkn0j@anonaddy.me> Date: Fri, 31 May 2024 01:41:35 +0000 Subject: [PATCH 4/5] small comment clarity --- src/classes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes.ts b/src/classes.ts index d95f4b5..bd6fa96 100644 --- a/src/classes.ts +++ b/src/classes.ts @@ -19,7 +19,7 @@ export type { SerializedJsonRpcError }; export class JsonRpcError< Data extends OptionalDataWithOptionalCause, > extends Error { - // The `cause` field can be removed when tsconfig lib and/or target have changed to >=es2022 + // The `cause` definition can be removed when tsconfig lib and/or target have changed to >=es2022 public cause?: unknown; public code: number; From 08108c535cf5ee25603715cbfae0cd9aa3f2dea6 Mon Sep 17 00:00:00 2001 From: legobt <6wbvkn0j@anonaddy.me> Date: Fri, 31 May 2024 01:58:22 +0000 Subject: [PATCH 5/5] 6.3.0-rc1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5af061d..2e83f07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/rpc-errors", - "version": "6.2.1", + "version": "6.3.0-rc1", "description": "Ethereum RPC and Provider errors", "keywords": [ "rpc",