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: sendAsync & errors #9

Merged
merged 5 commits into from
Mar 12, 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
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
Loading