Skip to content

Commit

Permalink
feat: propagate data.cause as cause in JsonRpcError constructor (#140)
Browse files Browse the repository at this point in the history
* test: add cases for cause propagation
  - break out util function dataHasCause with test
  - use native causes when available
  - adding explicit cause field because we can not yet use es2022
  • Loading branch information
legobeat authored May 31, 2024
1 parent 1c1ffa9 commit 1b44940
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 11 deletions.
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 92.64,
functions: 94.44,
lines: 92.85,
statements: 92.85,
branches: 91.89,
functions: 94.59,
lines: 92.42,
statements: 92.42,
},
},

Expand Down
6 changes: 5 additions & 1 deletion src/__fixtures__/errors.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand Down
23 changes: 19 additions & 4 deletions src/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import type {
Json,
JsonRpcError as SerializedJsonRpcError,
} from '@metamask/utils';
import { isPlainObject } from '@metamask/utils';
import { hasProperty, 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 };

Expand All @@ -19,6 +19,9 @@ export type { SerializedJsonRpcError };
export class JsonRpcError<
Data extends OptionalDataWithOptionalCause,
> extends Error {
// The `cause` definition can be removed when tsconfig lib and/or target have changed to >=es2022
public cause?: unknown;

public code: number;

public data?: Data;
Expand All @@ -32,11 +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 (!hasProperty(this, 'cause')) {
Object.assign(this, { cause: data.cause });
}
} else {
super(message);
}

if (data !== undefined) {
this.data = data;
}

this.code = code;
}

/**
Expand Down
32 changes: 32 additions & 0 deletions src/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
22 changes: 21 additions & 1 deletion src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
dummyMessage,
dummyData,
} from './__fixtures__';
import { getMessageFromCode, serializeError } from './utils';
import { dataHasCause, getMessageFromCode, serializeError } from './utils';

const rpcCodes = errorCodes.rpc;

Expand Down Expand Up @@ -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);
});
});
13 changes: 13 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"esModuleInterop": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
// Reminder: Remove custom `cause` field from JsonRpcError when enabling es2022
"lib": ["ES2020"],
"module": "CommonJS",
"moduleResolution": "node",
Expand Down

0 comments on commit 1b44940

Please sign in to comment.