diff --git a/package.json b/package.json index 3461d83..b106a9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rainbow-me/provider", - "version": "0.0.6", + "version": "0.0.7", "main": "dist/index.js", "license": "MIT", "files": [ diff --git a/src/RainbowProvider.ts b/src/RainbowProvider.ts index d1df3f4..8732e88 100644 --- a/src/RainbowProvider.ts +++ b/src/RainbowProvider.ts @@ -4,6 +4,7 @@ import { IMessenger, IProviderRequestTransport, RequestArguments, + RequestError, RequestResponse, } from './references/messengers'; @@ -110,8 +111,28 @@ export class RainbowProvider extends EventEmitter { /** @deprecated – This method is deprecated in favor of `request`. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - async sendAsync(args: RequestArguments) { - return this.request(args); + async sendAsync( + args: RequestArguments, + callback: (error: RequestError | null, response: RequestResponse) => void, + ) { + try { + const result = await this.request(args); + callback(null, { + id: args.id!, + jsonrpc: '2.0', + result, + }); + } catch (error: unknown) { + callback(error as Error, { + id: args.id!, + jsonrpc: '2.0', + error: { + code: (error as RequestError).code, + message: (error as RequestError).message, + name: (error as RequestError).name, + }, + }); + } } /** @deprecated – This method is deprecated in favor of `request`. */ diff --git a/src/handleProviderRequest.test.ts b/src/handleProviderRequest.test.ts index 286006a..48ab9e8 100644 --- a/src/handleProviderRequest.test.ts +++ b/src/handleProviderRequest.test.ts @@ -155,16 +155,18 @@ describe('handleProviderRequest', () => { }); it('should rate limit requests', async () => { - checkRateLimitMock.mockImplementationOnce(() => - Promise.resolve({ id: 1, error: new Error('Rate Limit Exceeded') }), - ); + checkRateLimitMock.mockImplementationOnce(() => Promise.resolve(true)); const response = await transport.send( { id: 1, method: 'eth_requestAccounts' }, { id: 1 }, ); expect(response).toEqual({ id: 1, - error: new Error('Rate Limit Exceeded'), + error: { + code: -32005, + message: 'Rate Limit Exceeded', + name: 'Limit exceeded', + }, }); }); diff --git a/src/handleProviderRequest.ts b/src/handleProviderRequest.ts index f3b6caa..f55df59 100644 --- a/src/handleProviderRequest.ts +++ b/src/handleProviderRequest.ts @@ -13,9 +13,33 @@ import { CallbackOptions, IProviderRequestTransport, ProviderRequestPayload, + RequestError, } from './references/messengers'; import { ActiveSession } from './references/appSession'; import { toHex } from './utils/hex'; +import { errorCodes } from './references/errorCodes'; + +const buildError = ({ + id, + message, + errorCode, +}: { + id: number; + errorCode: { + code: number; + name: string; + }; + message?: string; +}): { id: number; error: RequestError } => { + return { + id, + error: { + name: errorCode.name, + message, + code: errorCode.code, + }, + }; +}; export const handleProviderRequest = ({ providerRequestTransport, @@ -74,7 +98,11 @@ export const handleProviderRequest = ({ try { const rateLimited = await checkRateLimit({ id, meta, method }); if (rateLimited) { - return { id, error: new Error('Rate Limit Exceeded') }; + return buildError({ + id, + message: 'Rate Limit Exceeded', + errorCode: errorCodes.LIMIT_EXCEEDED, + }); } const url = meta?.sender?.url || ''; @@ -187,7 +215,11 @@ export const handleProviderRequest = ({ chainId !== undefined && Number(chainId) !== Number(activeSession?.chainId) ) { - throw new Error('ChainId mismatch'); + return buildError({ + id, + message: 'Chain Id mismatch', + errorCode: errorCodes.INVALID_REQUEST, + }); } } @@ -206,7 +238,13 @@ export const handleProviderRequest = ({ const featureFlags = getFeatureFlags(); if (!featureFlags.custom_rpc) { const supportedChain = isSupportedChain?.(proposedChainId); - if (!supportedChain) throw new Error('Chain Id not supported'); + if (!supportedChain) { + return buildError({ + id, + message: 'Chain Id not supported', + errorCode: errorCodes.INVALID_REQUEST, + }); + } } else { const { chainId, @@ -217,49 +255,66 @@ export const handleProviderRequest = ({ // Validate chain Id if (!isHex(chainId)) { - throw new Error( - `Expected 0x-prefixed, unpadded, non-zero hexadecimal string "chainId". Received: ${chainId}`, - ); + return buildError({ + id, + message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string "chainId". Received: ${chainId}`, + errorCode: errorCodes.INVALID_INPUT, + }); } else if (Number(chainId) > Number.MAX_SAFE_INTEGER) { - throw new Error( - `Invalid chain ID "${chainId}": numerical value greater than max safe value. Received: ${chainId}`, - ); + return buildError({ + id, + message: `Invalid chain ID "${chainId}": numerical value greater than max safe value. Received: ${chainId}`, + errorCode: errorCodes.INVALID_INPUT, + }); // Validate symbol and name } else if (!rpcUrl) { - throw new Error( - `Expected non-empty array[string] "rpcUrls". Received: ${rpcUrl}`, - ); + return buildError({ + id, + message: `Expected non-empty array[string] "rpcUrls". Received: ${rpcUrl}`, + errorCode: errorCodes.INVALID_INPUT, + }); } else if (!name || !symbol) { - throw new Error( - 'Expected non-empty string "nativeCurrency.name", "nativeCurrency.symbol"', - ); + return buildError({ + id, + message: + 'Expected non-empty string "nativeCurrency.name", "nativeCurrency.symbol"', + errorCode: errorCodes.INVALID_INPUT, + }); // Validate decimals } else if ( !Number.isInteger(decimals) || decimals < 0 || decimals > 36 ) { - throw new Error( - `Expected non-negative integer "nativeCurrency.decimals" less than 37. Received: ${decimals}`, - ); + return buildError({ + id, + message: `Expected non-negative integer "nativeCurrency.decimals" less than 37. Received: ${decimals}`, + errorCode: errorCodes.INVALID_INPUT, + }); // Validate symbol length } else if (symbol.length < 2 || symbol.length > 6) { - throw new Error( - `Expected 2-6 character string 'nativeCurrency.symbol'. Received: ${symbol}`, - ); + return buildError({ + id, + message: `Expected 2-6 character string 'nativeCurrency.symbol'. Received: ${symbol}`, + errorCode: errorCodes.INVALID_INPUT, + }); // Validate symbol against existing chains } else if (isSupportedChain?.(Number(chainId))) { const knownChain = getChain(Number(chainId)); if (knownChain?.nativeCurrency.symbol !== symbol) { - throw new Error( - `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received: ${symbol}`, - ); + return buildError({ + id, + message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received: ${symbol}`, + errorCode: errorCodes.INVALID_INPUT, + }); } // Validate blockExplorerUrl } else if (!blockExplorerUrl) { - throw new Error( - `Expected null or array with at least one valid string HTTPS URL 'blockExplorerUrl'. Received: ${blockExplorerUrl}`, - ); + return buildError({ + id, + message: `Expected null or array with at least one valid string HTTPS URL 'blockExplorerUrl'. Received: ${blockExplorerUrl}`, + errorCode: errorCodes.INVALID_INPUT, + }); } const { chainAlreadyAdded } = onAddEthereumChain({ proposedChain, @@ -277,7 +332,11 @@ export const handleProviderRequest = ({ // PER EIP - return null if the network was added otherwise throw if (!response) { - throw new Error('User rejected the request.'); + return buildError({ + id, + message: 'User rejected the request.', + errorCode: errorCodes.TRANSACTION_REJECTED, + }); } else { response = null; } @@ -295,7 +354,11 @@ export const handleProviderRequest = ({ proposedChain, callbackOptions: meta, }); - throw new Error('Chain Id not supported'); + return buildError({ + id, + message: 'Chain Id not supported', + errorCode: errorCodes.INVALID_REQUEST, + }); } else { onSwitchEthereumChainSupported?.({ proposedChain, @@ -323,11 +386,19 @@ export const handleProviderRequest = ({ }; }; if (type !== 'ERC20') { - throw new Error('Method supported only for ERC20'); + return buildError({ + id, + message: 'Method supported only for ERC20', + errorCode: errorCodes.METHOD_NOT_SUPPORTED, + }); } if (!address) { - throw new Error('Address is required'); + return buildError({ + id, + message: 'Address is required', + errorCode: errorCodes.INVALID_INPUT, + }); } let chainId: number | null = null; @@ -390,12 +461,20 @@ export const handleProviderRequest = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any response = await provider.send(method, params as any[]); } catch (e) { - throw new Error('Method not supported'); + return buildError({ + id, + message: 'Method not supported', + errorCode: errorCodes.METHOD_NOT_SUPPORTED, + }); } } } return { id, result: response }; } catch (error) { - return { id, error: error }; + return buildError({ + id, + message: (error as Error).message, + errorCode: errorCodes.INTERNAL_ERROR, + }); } }); diff --git a/src/references/errorCodes.ts b/src/references/errorCodes.ts new file mode 100644 index 0000000..90ccc64 --- /dev/null +++ b/src/references/errorCodes.ts @@ -0,0 +1,51 @@ +// https://eips.ethereum.org/EIPS/eip-1474 +export const errorCodes = { + PARSE_ERROR: { + code: -32700, + name: 'Parse error', + }, // Invalid JSON + INVALID_REQUEST: { + code: -32600, + name: 'Invalid Request', + }, // JSON is not a valid request object + METHOD_NOT_FOUND: { + code: -32601, + name: 'Method not found', + }, // Method does not exist + INVALID_PARAMS: { + code: -32602, + name: 'Invalid params', + }, // Invalid method parameters + INTERNAL_ERROR: { + code: -32603, + name: 'Internal error', + }, // Internal JSON-RPC error + INVALID_INPUT: { + code: -32000, + name: 'Invalid input', + }, // Missing or invalid parameters + RESOURCE_NOT_FOUND: { + code: -32001, + name: 'Resource not found', + }, // Requested resource not found + RESOURCE_UNAVAILABLE: { + code: -32002, + name: 'Resource unavailable', + }, // Requested resource not available + TRANSACTION_REJECTED: { + code: -32003, + name: 'Transaction rejected', + }, // Transaction creation failed + METHOD_NOT_SUPPORTED: { + code: -32004, + name: 'Method not supported', + }, // Method is not implemented + LIMIT_EXCEEDED: { + code: -32005, + name: 'Limit exceeded', + }, // Request exceeds defined limit + JSON_RPC_VERSION_NOT_SUPPORTED: { + code: -32006, + name: 'JSON-RPC version not supported', + }, // Version of JSON-RPC protocol is not supported +}; diff --git a/src/references/messengers.ts b/src/references/messengers.ts index c00633a..ad221eb 100644 --- a/src/references/messengers.ts +++ b/src/references/messengers.ts @@ -6,15 +6,19 @@ export type RequestArguments = { params?: Array; }; +export type RequestError = { name: string; message?: string; code?: number }; + export type RequestResponse = | { id: number; - error: Error; + error?: RequestError; + jsonrpc?: string; result?: never; } | { id: number; - error?: never; + error?: RequestError; + jsonrpc?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any result: any; };