Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow passing a cause to predefined error functions #83

Merged
merged 6 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ module.exports = {
global: {
branches: 94.5,
functions: 85.36,
lines: 96.88,
statements: 96.88,
lines: 97.08,
statements: 97.08,
},
},

Expand Down
24 changes: 20 additions & 4 deletions src/classes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import safeStringify from 'fast-safe-stringify';
import { Json, JsonRpcError as SerializedJsonRpcError } from '@metamask/utils';
import {
isPlainObject,
Json,
JsonRpcError as SerializedJsonRpcError,
} from '@metamask/utils';
import { DataWithOptionalCause, serializeCause } from './utils';

export { SerializedJsonRpcError };

Expand All @@ -9,7 +14,7 @@ export { SerializedJsonRpcError };
*
* Permits any integer error code.
*/
export class JsonRpcError<T extends Json> extends Error {
export class JsonRpcError<T extends DataWithOptionalCause> extends Error {
public code: number;

public data?: T;
Expand Down Expand Up @@ -40,13 +45,22 @@ export class JsonRpcError<T extends Json> extends Error {
code: this.code,
message: this.message,
};

if (this.data !== undefined) {
serialized.data = this.data;
// `this.data` is not guaranteed to be a plain object, but this simplifies
// the type guard below. We can safely cast it because we know it's a
// JSON-serializable value.
serialized.data = this.data as { [key: string]: Json };

if (isPlainObject(this.data)) {
serialized.data.cause = serializeCause(this.data.cause);
}
}

if (this.stack) {
serialized.stack = this.stack;
}

return serialized;
}

Expand All @@ -65,7 +79,9 @@ export class JsonRpcError<T extends Json> extends Error {
* Error subclass implementing Ethereum Provider errors per EIP-1193.
* Permits integer error codes in the [ 1000 <= 4999 ] range.
*/
export class EthereumProviderError<T extends Json> extends JsonRpcError<T> {
export class EthereumProviderError<
T extends DataWithOptionalCause,
> extends JsonRpcError<T> {
/**
* Create an Ethereum Provider JSON-RPC error.
*
Expand Down
50 changes: 43 additions & 7 deletions src/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('custom provider error options', () => {
});
});

describe('ethError.rpc.server', () => {
describe('rpcErrors.server', () => {
it('throws on invalid input', () => {
expect(() => {
// @ts-expect-error Invalid input
Expand All @@ -63,6 +63,16 @@ describe('ethError.rpc.server', () => {
rpcErrors.server({ code: 1 });
}).toThrow('"code" must be an integer such that: -32099 <= code <= -32005');
});

it('returns appropriate value', () => {
const error = rpcErrors.server({
code: SERVER_ERROR_CODE,
data: Object.assign({}, dummyData),
});

expect(error.code <= -32000 && error.code >= -32099).toBe(true);
expect(error.message).toBe(JSON_RPC_SERVER_ERROR_MESSAGE);
});
});

describe('rpcErrors', () => {
Expand All @@ -85,14 +95,22 @@ describe('rpcErrors', () => {
},
);

it('server returns appropriate value', () => {
const error = rpcErrors.server({
code: SERVER_ERROR_CODE,
data: Object.assign({}, dummyData),
it('serializes a cause', () => {
const error = rpcErrors.invalidInput({
data: {
foo: 'bar',
cause: new Error('foo'),
},
});

expect(error.code <= -32000 && error.code >= -32099).toBe(true);
expect(error.message).toBe(JSON_RPC_SERVER_ERROR_MESSAGE);
expect(error.serialize().data).not.toBeInstanceOf(Error);
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
expect(error.serialize().data).toStrictEqual({
foo: 'bar',
cause: {
message: 'foo',
stack: expect.stringContaining('Error: foo'),
},
});
});
});

Expand Down Expand Up @@ -123,4 +141,22 @@ describe('providerErrors', () => {
expect(error.code).toBe(CUSTOM_ERROR_CODE);
expect(error.message).toBe(CUSTOM_ERROR_MESSAGE);
});

it('serializes a cause', () => {
const error = providerErrors.unauthorized({
data: {
foo: 'bar',
cause: new Error('foo'),
},
});

expect(error.serialize().data).not.toBeInstanceOf(Error);
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
expect(error.serialize().data).toStrictEqual({
foo: 'bar',
cause: {
message: 'foo',
stack: expect.stringContaining('Error: foo'),
},
});
});
});
83 changes: 50 additions & 33 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Json } from '@metamask/utils';
import { JsonRpcError, EthereumProviderError } from './classes';
import { getMessageFromCode } from './utils';
import { DataWithOptionalCause, getMessageFromCode } from './utils';
import { errorCodes } from './error-constants';

type EthereumErrorOptions<T extends Json> = {
type EthereumErrorOptions<T extends DataWithOptionalCause> = {
message?: string;
data?: T;
};

type ServerErrorOptions<T extends Json> = {
type ServerErrorOptions<T extends DataWithOptionalCause> = {
code: number;
} & EthereumErrorOptions<T>;

type CustomErrorArg<T extends Json> = ServerErrorOptions<T>;
type CustomErrorArg<T extends DataWithOptionalCause> = ServerErrorOptions<T>;

type JsonRpcErrorsArg<T extends Json> = EthereumErrorOptions<T> | string;
type JsonRpcErrorsArg<T extends DataWithOptionalCause> =
| EthereumErrorOptions<T>
| string;

export const rpcErrors = {
/**
Expand All @@ -23,7 +24,7 @@ export const rpcErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
parse: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
parse: <T extends DataWithOptionalCause>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.parse, arg),

/**
Expand All @@ -32,16 +33,17 @@ export const rpcErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
invalidRequest: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.invalidRequest, arg),
invalidRequest: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => getJsonRpcError(errorCodes.rpc.invalidRequest, arg),

/**
* Get a JSON RPC 2.0 Invalid Params (-32602) error.
*
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
invalidParams: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
invalidParams: <T extends DataWithOptionalCause>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.invalidParams, arg),

/**
Expand All @@ -50,16 +52,17 @@ export const rpcErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
methodNotFound: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.methodNotFound, arg),
methodNotFound: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => getJsonRpcError(errorCodes.rpc.methodNotFound, arg),

/**
* Get a JSON RPC 2.0 Internal (-32603) error.
*
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
internal: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
internal: <T extends DataWithOptionalCause>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.internal, arg),

/**
Expand All @@ -70,7 +73,7 @@ export const rpcErrors = {
* @param opts - The error options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
server: <T extends Json>(opts: ServerErrorOptions<T>) => {
server: <T extends DataWithOptionalCause>(opts: ServerErrorOptions<T>) => {
if (!opts || typeof opts !== 'object' || Array.isArray(opts)) {
throw new Error(
'Ethereum RPC Server errors must provide single object argument.',
Expand All @@ -91,7 +94,7 @@ export const rpcErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
invalidInput: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
invalidInput: <T extends DataWithOptionalCause>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.invalidInput, arg),

/**
Expand All @@ -100,43 +103,47 @@ export const rpcErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
resourceNotFound: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.resourceNotFound, arg),
resourceNotFound: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => getJsonRpcError(errorCodes.rpc.resourceNotFound, arg),

/**
* Get an Ethereum JSON RPC Resource Unavailable (-32002) error.
*
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
resourceUnavailable: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.resourceUnavailable, arg),
resourceUnavailable: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => getJsonRpcError(errorCodes.rpc.resourceUnavailable, arg),

/**
* Get an Ethereum JSON RPC Transaction Rejected (-32003) error.
*
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
transactionRejected: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.transactionRejected, arg),
transactionRejected: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => getJsonRpcError(errorCodes.rpc.transactionRejected, arg),

/**
* Get an Ethereum JSON RPC Method Not Supported (-32004) error.
*
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
methodNotSupported: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.methodNotSupported, arg),
methodNotSupported: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => getJsonRpcError(errorCodes.rpc.methodNotSupported, arg),

/**
* Get an Ethereum JSON RPC Limit Exceeded (-32005) error.
*
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
limitExceeded: <T extends Json>(arg?: JsonRpcErrorsArg<T>) =>
limitExceeded: <T extends DataWithOptionalCause>(arg?: JsonRpcErrorsArg<T>) =>
getJsonRpcError(errorCodes.rpc.limitExceeded, arg),
};

Expand All @@ -147,7 +154,9 @@ export const providerErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link EthereumProviderError} class.
*/
userRejectedRequest: <T extends Json>(arg?: JsonRpcErrorsArg<T>) => {
userRejectedRequest: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => {
return getEthProviderError(errorCodes.provider.userRejectedRequest, arg);
},

Expand All @@ -157,7 +166,9 @@ export const providerErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link EthereumProviderError} class.
*/
unauthorized: <T extends Json>(arg?: JsonRpcErrorsArg<T>) => {
unauthorized: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => {
return getEthProviderError(errorCodes.provider.unauthorized, arg);
},

Expand All @@ -167,7 +178,9 @@ export const providerErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link EthereumProviderError} class.
*/
unsupportedMethod: <T extends Json>(arg?: JsonRpcErrorsArg<T>) => {
unsupportedMethod: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => {
return getEthProviderError(errorCodes.provider.unsupportedMethod, arg);
},

Expand All @@ -177,7 +190,9 @@ export const providerErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link EthereumProviderError} class.
*/
disconnected: <T extends Json>(arg?: JsonRpcErrorsArg<T>) => {
disconnected: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => {
return getEthProviderError(errorCodes.provider.disconnected, arg);
},

Expand All @@ -187,7 +202,9 @@ export const providerErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link EthereumProviderError} class.
*/
chainDisconnected: <T extends Json>(arg?: JsonRpcErrorsArg<T>) => {
chainDisconnected: <T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
) => {
return getEthProviderError(errorCodes.provider.chainDisconnected, arg);
},

Expand All @@ -197,7 +214,7 @@ export const providerErrors = {
* @param opts - The error options bag.
* @returns An instance of the {@link EthereumProviderError} class.
*/
custom: <T extends Json>(opts: CustomErrorArg<T>) => {
custom: <T extends DataWithOptionalCause>(opts: CustomErrorArg<T>) => {
if (!opts || typeof opts !== 'object' || Array.isArray(opts)) {
throw new Error(
'Ethereum Provider custom errors must provide single object argument.',
Expand All @@ -220,7 +237,7 @@ export const providerErrors = {
* @param arg - The error message or options bag.
* @returns An instance of the {@link JsonRpcError} class.
*/
function getJsonRpcError<T extends Json>(
function getJsonRpcError<T extends DataWithOptionalCause>(
code: number,
arg?: JsonRpcErrorsArg<T>,
): JsonRpcError<T> {
Expand All @@ -235,7 +252,7 @@ function getJsonRpcError<T extends Json>(
* @param arg - The error message or options bag.
* @returns An instance of the {@link EthereumProviderError} class.
*/
function getEthProviderError<T extends Json>(
function getEthProviderError<T extends DataWithOptionalCause>(
code: number,
arg?: JsonRpcErrorsArg<T>,
): EthereumProviderError<T> {
Expand All @@ -253,7 +270,7 @@ function getEthProviderError<T extends Json>(
* @param arg - The error message or options bag.
* @returns A tuple containing the error message and optional data.
*/
function parseOpts<T extends Json>(
function parseOpts<T extends DataWithOptionalCause>(
arg?: JsonRpcErrorsArg<T>,
): [message?: string | undefined, data?: T | undefined] {
if (arg) {
Expand Down
Loading