Skip to content

Commit

Permalink
fix: sendAsync & errors (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
estebanmino authored Mar 12, 2024
1 parent 1eb2876 commit e00264c
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 42 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rainbow-me/provider",
"version": "0.0.6",
"version": "0.0.7",
"main": "dist/index.js",
"license": "MIT",
"files": [
Expand Down
25 changes: 23 additions & 2 deletions src/RainbowProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
IMessenger,
IProviderRequestTransport,
RequestArguments,
RequestError,
RequestResponse,
} from './references/messengers';

Expand Down Expand Up @@ -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`. */
Expand Down
10 changes: 6 additions & 4 deletions src/handleProviderRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
});
});

Expand Down
145 changes: 112 additions & 33 deletions src/handleProviderRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -74,7 +98,11 @@ export const handleProviderRequest = ({
try {
const rateLimited = await checkRateLimit({ id, meta, method });
if (rateLimited) {
return { id, error: <Error>new Error('Rate Limit Exceeded') };
return buildError({
id,
message: 'Rate Limit Exceeded',
errorCode: errorCodes.LIMIT_EXCEEDED,
});
}

const url = meta?.sender?.url || '';
Expand Down Expand Up @@ -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,
});
}
}

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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;
}
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>error };
return buildError({
id,
message: (error as Error).message,
errorCode: errorCodes.INTERNAL_ERROR,
});
}
});
51 changes: 51 additions & 0 deletions src/references/errorCodes.ts
Original file line number Diff line number Diff line change
@@ -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
};
8 changes: 6 additions & 2 deletions src/references/messengers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ export type RequestArguments = {
params?: Array<unknown>;
};

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;
};
Expand Down

0 comments on commit e00264c

Please sign in to comment.