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

fix: ensure that errors are JSON-serializable #162

Merged
merged 4 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
81 changes: 48 additions & 33 deletions src/rpc-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Keyring } from './api';
import { KeyringRpcMethod, isKeyringRpcMethod } from './internal/rpc';
import type { JsonRpcRequest } from './JsonRpcRequest';
import { MethodNotSupportedError, handleKeyringRequest } from './rpc-handler';
import { handleKeyringRequest } from './rpc-handler';

describe('keyringRpcDispatcher', () => {
describe('handleKeyringRequest', () => {
const keyring = {
listAccounts: jest.fn(),
getAccount: jest.fn(),
Expand All @@ -23,7 +23,7 @@ describe('keyringRpcDispatcher', () => {
jest.clearAllMocks();
});

it('should call keyring_listAccounts', async () => {
it('calls `keyring_listAccounts`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -37,7 +37,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('ListAccounts result');
});

it('should fail to call keyringRpcDispatcher with a non-JSON-RPC request', async () => {
it('fails to execute an mal-formatted JSON-RPC request', async () => {
const request = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -51,7 +51,7 @@ describe('keyringRpcDispatcher', () => {
);
});

it('should call keyring_getAccount', async () => {
it('calls `keyring_getAccount`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -68,7 +68,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('GetAccount result');
});

it('should fail to call keyring_getAccount without the account ID', async () => {
it('fails to call `keyring_getAccount` without providing an account ID', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -81,7 +81,7 @@ describe('keyringRpcDispatcher', () => {
);
});

it('should fail to call keyring_getAccount without params', async () => {
it('fails to call `keyring_getAccount` when the `params` is not provided', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -93,7 +93,7 @@ describe('keyringRpcDispatcher', () => {
);
});

it('should call keyring_createAccount', async () => {
it('calls `keyring_createAccount`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -108,7 +108,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('CreateAccount result');
});

it('should call keyring_filterAccountChains', async () => {
it('calls `keyring_filterAccountChains`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -131,7 +131,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('FilterSupportedChains result');
});

it('should call keyring_updateAccount', async () => {
it('calls `keyring_updateAccount`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand Down Expand Up @@ -160,7 +160,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('UpdateAccount result');
});

it('should call keyring_deleteAccount', async () => {
it('calls `keyring_deleteAccount`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -177,7 +177,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('DeleteAccount result');
});

it('should call keyring_exportAccount', async () => {
it('calls `keyring_exportAccount`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -197,7 +197,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toStrictEqual(expected);
});

it('should throw MethodNotSupportedError if exportAccount is not implemented', async () => {
it('throws an error if `keyring_exportAccount` is not implemented', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -211,11 +211,11 @@ describe('keyringRpcDispatcher', () => {
delete partialKeyring.exportAccount;

await expect(handleKeyringRequest(partialKeyring, request)).rejects.toThrow(
MethodNotSupportedError,
'Method not supported: keyring_exportAccount',
);
});

it('should call keyring_listRequests', async () => {
it('calls `keyring_listRequests`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -229,7 +229,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('ListRequests result');
});

it('should throw MethodNotSupportedError if listRequests is not implemented', async () => {
it('throws an error if `keyring_listRequests` is not implemented', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -242,11 +242,11 @@ describe('keyringRpcDispatcher', () => {
delete partialKeyring.listRequests;

await expect(handleKeyringRequest(partialKeyring, request)).rejects.toThrow(
MethodNotSupportedError,
'Method not supported: keyring_listRequests',
);
});

it('should call keyring_getRequest', async () => {
it('calls `keyring_getRequest`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -263,7 +263,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('GetRequest result');
});

it('should throw MethodNotSupportedError if getRequest is not implemented', async () => {
it('throws an error if `keyring_getRequest` is not implemented', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -277,11 +277,11 @@ describe('keyringRpcDispatcher', () => {
delete partialKeyring.getRequest;

await expect(handleKeyringRequest(partialKeyring, request)).rejects.toThrow(
MethodNotSupportedError,
'Method not supported: keyring_getRequest',
);
});

it('should call keyring_submitRequest', async () => {
it('calls `keyring_submitRequest`', async () => {
const dappRequest = {
id: 'c555de37-cf4b-4ff2-8273-39db7fb58f1c',
scope: 'eip155:1',
Expand All @@ -306,7 +306,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('SubmitRequest result');
});

it('should call keyring_approveRequest', async () => {
it('calls `keyring_approveRequest`', async () => {
const payload = {
id: '59db4ff8-8eb3-4a75-8ef3-b80aff8fa780',
data: { signature: '0x0123' },
Expand All @@ -328,7 +328,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('ApproveRequest result');
});

it('should throw MethodNotSupportedError if approveRequest is not implemented', async () => {
it('throws an error if `keyring_approveRequest` is not implemented', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -342,11 +342,11 @@ describe('keyringRpcDispatcher', () => {
delete partialKeyring.approveRequest;

await expect(handleKeyringRequest(partialKeyring, request)).rejects.toThrow(
MethodNotSupportedError,
'Method not supported: keyring_approveRequest',
);
});

it('calls the keyring with a non-UUIDv4 string request ID', async () => {
it('calls a method with a non-UUIDv4 string as the request ID', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: 'request-id',
Expand All @@ -357,7 +357,7 @@ describe('keyringRpcDispatcher', () => {
expect(await handleKeyringRequest(keyring, request)).toStrictEqual([]);
});

it('calls the keyring with a number request ID', async () => {
it('calls the keyring with a number as the request ID', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: 1,
Expand All @@ -368,7 +368,7 @@ describe('keyringRpcDispatcher', () => {
expect(await handleKeyringRequest(keyring, request)).toStrictEqual([]);
});

it('calls the keyring with a null request ID', async () => {
it('calls the keyring with null as the request ID', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: null,
Expand All @@ -379,7 +379,7 @@ describe('keyringRpcDispatcher', () => {
expect(await handleKeyringRequest(keyring, request)).toStrictEqual([]);
});

it('fails to call the keyring with a boolean request ID', async () => {
it('fails to call the keyring with a boolean as tne request ID', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: true as any,
Expand All @@ -392,7 +392,7 @@ describe('keyringRpcDispatcher', () => {
);
});

it('should call keyring_rejectRequest', async () => {
it('calls `keyring_rejectRequest`', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -409,7 +409,7 @@ describe('keyringRpcDispatcher', () => {
expect(result).toBe('RejectRequest result');
});

it('should throw MethodNotSupportedError if rejectRequest is not implemented', async () => {
it('throws an error if `keyring_rejectRequest` is not implemented', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
Expand All @@ -423,19 +423,34 @@ describe('keyringRpcDispatcher', () => {
delete partialKeyring.rejectRequest;

await expect(handleKeyringRequest(partialKeyring, request)).rejects.toThrow(
MethodNotSupportedError,
'Method not supported: keyring_rejectRequest',
);
});

it('should throw MethodNotSupportedError for an unknown method', async () => {
it('throws an error if an unknown method is called', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4',
method: 'unknown_method',
};

await expect(handleKeyringRequest(keyring, request)).rejects.toThrow(
MethodNotSupportedError,
'Method not supported: unknown_method',
);
});

it('throws an "unknown error" if the error message is not a string', async () => {
const request: JsonRpcRequest = {
jsonrpc: '2.0',
id: '80c25a6b-4a76-44f4-88c5-7b3b76f72a74',
method: 'keyring_listAccounts',
};

const error = new Error();
error.message = 1 as unknown as string;
keyring.listAccounts.mockRejectedValue(error);
await expect(handleKeyringRequest(keyring, request)).rejects.toThrow(
'An unknown error occurred while handling the keyring request',
);
});
});
Expand Down
40 changes: 38 additions & 2 deletions src/rpc-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ export class MethodNotSupportedError extends Error {
}

/**
* Handles a keyring JSON-RPC request.
* Inner function that dispatches JSON-RPC request to the associated Keyring
* methods.
*
* @param keyring - Keyring instance.
* @param request - Keyring JSON-RPC request.
* @returns A promise that resolves to the keyring response.
*/
export async function handleKeyringRequest(
async function dispatchRequest(
keyring: Keyring,
request: JsonRpcRequest,
): Promise<Json | void> {
Expand Down Expand Up @@ -128,3 +129,38 @@ export async function handleKeyringRequest(
}
}
}

/**
* Handles a keyring JSON-RPC request.
*
* This function is meant to be used as a handler for Keyring JSON-RPC requests
* in an Accounts Snap.
*
* @param keyring - Keyring instance.
* @param request - Keyring JSON-RPC request.
* @returns A promise that resolves to the keyring response.
* @example
* ```ts
* export const onKeyringRequest: OnKeyringRequestHandler = async ({
* origin,
* request,
* }) => {
* return await handleKeyringRequest(keyring, request);
* };
* ```
*/
export async function handleKeyringRequest(
keyring: Keyring,
request: JsonRpcRequest,
): Promise<Json | void> {
try {
return await dispatchRequest(keyring, request);
} catch (error) {
const message =
error instanceof Error && typeof error.message === 'string'
? error.message
: 'An unknown error occurred while handling the keyring request';

throw new Error(message);
}
}
Loading