Skip to content

Commit e6a9162

Browse files
Allow passing a cause to predefined error functions (#83)
* Allow passing a cause to predefined error functions This allows passing an `Error` class object to one of the predefined error functions as defined in `rpcErrors` and `providerErrors`. Upon calling the `serialise` function of the `JsonRpcError` class, the cause error will be serialised using the `serializeCause` function. * Add tests to ensure error.serialize().data is not an instance of Error The expect function ensures that the type of error.serialize().data is an object and contains specific serialized data while ensuring it does not contain an error instance. This test is necessary to avoid unexpected errors during exception handling. * Apply suggestions from code review Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com> * Export `serializeCause` and `DataWithOptionalCause` * Fix type errors * Update coverage tresholds --------- Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>
1 parent 7471a50 commit e6a9162

File tree

6 files changed

+151
-64
lines changed

6 files changed

+151
-64
lines changed

jest.config.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ module.exports = {
2222
collectCoverage: true,
2323

2424
// An array of glob patterns indicating a set of files for which coverage information should be collected
25-
collectCoverageFrom: ['./src/**/*.ts', '!./src/__fixtures__/**/*'],
25+
collectCoverageFrom: [
26+
'./src/**/*.ts',
27+
'!./src/__fixtures__/**/*',
28+
'!./src/index.ts',
29+
],
2630

2731
// The directory where Jest should output its coverage files
2832
coverageDirectory: 'coverage',
@@ -41,10 +45,10 @@ module.exports = {
4145
// An object that configures minimum threshold enforcement for coverage results
4246
coverageThreshold: {
4347
global: {
44-
branches: 94.5,
45-
functions: 85.36,
46-
lines: 96.88,
47-
statements: 96.88,
48+
branches: 94.25,
49+
functions: 94.11,
50+
lines: 97.02,
51+
statements: 97.02,
4852
},
4953
},
5054

src/classes.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import safeStringify from 'fast-safe-stringify';
2-
import { Json, JsonRpcError as SerializedJsonRpcError } from '@metamask/utils';
2+
import {
3+
isPlainObject,
4+
Json,
5+
JsonRpcError as SerializedJsonRpcError,
6+
} from '@metamask/utils';
7+
import { DataWithOptionalCause, serializeCause } from './utils';
38

49
export { SerializedJsonRpcError };
510

@@ -9,7 +14,7 @@ export { SerializedJsonRpcError };
914
*
1015
* Permits any integer error code.
1116
*/
12-
export class JsonRpcError<T extends Json> extends Error {
17+
export class JsonRpcError<T extends DataWithOptionalCause> extends Error {
1318
public code: number;
1419

1520
public data?: T;
@@ -40,13 +45,22 @@ export class JsonRpcError<T extends Json> extends Error {
4045
code: this.code,
4146
message: this.message,
4247
};
48+
4349
if (this.data !== undefined) {
44-
serialized.data = this.data;
50+
// `this.data` is not guaranteed to be a plain object, but this simplifies
51+
// the type guard below. We can safely cast it because we know it's a
52+
// JSON-serializable value.
53+
serialized.data = this.data as { [key: string]: Json };
54+
55+
if (isPlainObject(this.data)) {
56+
serialized.data.cause = serializeCause(this.data.cause);
57+
}
4558
}
4659

4760
if (this.stack) {
4861
serialized.stack = this.stack;
4962
}
63+
5064
return serialized;
5165
}
5266

@@ -65,7 +79,9 @@ export class JsonRpcError<T extends Json> extends Error {
6579
* Error subclass implementing Ethereum Provider errors per EIP-1193.
6680
* Permits integer error codes in the [ 1000 <= 4999 ] range.
6781
*/
68-
export class EthereumProviderError<T extends Json> extends JsonRpcError<T> {
82+
export class EthereumProviderError<
83+
T extends DataWithOptionalCause,
84+
> extends JsonRpcError<T> {
6985
/**
7086
* Create an Ethereum Provider JSON-RPC error.
7187
*

src/errors.test.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { assert, isPlainObject } from '@metamask/utils';
12
import { getMessageFromCode, JSON_RPC_SERVER_ERROR_MESSAGE } from './utils';
23
import {
34
dummyData,
@@ -45,7 +46,7 @@ describe('custom provider error options', () => {
4546
});
4647
});
4748

48-
describe('ethError.rpc.server', () => {
49+
describe('rpcErrors.server', () => {
4950
it('throws on invalid input', () => {
5051
expect(() => {
5152
// @ts-expect-error Invalid input
@@ -63,6 +64,16 @@ describe('ethError.rpc.server', () => {
6364
rpcErrors.server({ code: 1 });
6465
}).toThrow('"code" must be an integer such that: -32099 <= code <= -32005');
6566
});
67+
68+
it('returns appropriate value', () => {
69+
const error = rpcErrors.server({
70+
code: SERVER_ERROR_CODE,
71+
data: Object.assign({}, dummyData),
72+
});
73+
74+
expect(error.code <= -32000 && error.code >= -32099).toBe(true);
75+
expect(error.message).toBe(JSON_RPC_SERVER_ERROR_MESSAGE);
76+
});
6677
});
6778

6879
describe('rpcErrors', () => {
@@ -85,14 +96,26 @@ describe('rpcErrors', () => {
8596
},
8697
);
8798

88-
it('server returns appropriate value', () => {
89-
const error = rpcErrors.server({
90-
code: SERVER_ERROR_CODE,
91-
data: Object.assign({}, dummyData),
99+
it('serializes a cause', () => {
100+
const error = rpcErrors.invalidInput({
101+
data: {
102+
foo: 'bar',
103+
cause: new Error('foo'),
104+
},
92105
});
93106

94-
expect(error.code <= -32000 && error.code >= -32099).toBe(true);
95-
expect(error.message).toBe(JSON_RPC_SERVER_ERROR_MESSAGE);
107+
const serializedError = error.serialize();
108+
assert(serializedError.data);
109+
assert(isPlainObject(serializedError.data));
110+
111+
expect(serializedError.data.cause).not.toBeInstanceOf(Error);
112+
expect(serializedError.data).toStrictEqual({
113+
foo: 'bar',
114+
cause: {
115+
message: 'foo',
116+
stack: expect.stringContaining('Error: foo'),
117+
},
118+
});
96119
});
97120
});
98121

@@ -123,4 +146,26 @@ describe('providerErrors', () => {
123146
expect(error.code).toBe(CUSTOM_ERROR_CODE);
124147
expect(error.message).toBe(CUSTOM_ERROR_MESSAGE);
125148
});
149+
150+
it('serializes a cause', () => {
151+
const error = providerErrors.unauthorized({
152+
data: {
153+
foo: 'bar',
154+
cause: new Error('foo'),
155+
},
156+
});
157+
158+
const serializedError = error.serialize();
159+
assert(serializedError.data);
160+
assert(isPlainObject(serializedError.data));
161+
162+
expect(serializedError.data.cause).not.toBeInstanceOf(Error);
163+
expect(serializedError.data).toStrictEqual({
164+
foo: 'bar',
165+
cause: {
166+
message: 'foo',
167+
stack: expect.stringContaining('Error: foo'),
168+
},
169+
});
170+
});
126171
});

0 commit comments

Comments
 (0)