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

feat: increase rpc error information propagation #1213

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
27 changes: 26 additions & 1 deletion __tests__/rpcChannel.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RPC07 } from '../src';
import { LibraryError, RPC07, RpcError } from '../src';
import { createBlockForDevnet, getTestProvider } from './config/fixtures';
import { initializeMatcher } from './config/schema';

Expand All @@ -15,4 +15,29 @@ describe('RPC 0.7.0', () => {
const response = await channel.getBlockWithReceipts('latest');
expect(response).toMatchSchemaRef('BlockWithTxReceipts');
});

test('RPC error handling', async () => {
const fetchSpy = jest.spyOn(channel, 'fetch');
fetchSpy.mockResolvedValue({
json: async () => ({
jsonrpc: '2.0',
error: {
code: 24,
message: 'Block not found',
},
id: 0,
}),
} as any);

expect.assertions(3);
try {
// @ts-expect-error
await channel.fetchEndpoint('starknet_chainId');
} catch (error) {
expect(error).toBeInstanceOf(LibraryError);
expect(error).toBeInstanceOf(RpcError);
expect((error as RpcError).isType('BLOCK_NOT_FOUND')).toBe(true);
}
fetchSpy.mockRestore();
});
});
22 changes: 22 additions & 0 deletions __tests__/utils/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RPC, RpcError } from '../../src';

describe('Error utility tests', () => {
test('RpcError', () => {
const baseError: RPC.Errors.UNEXPECTED_ERROR = {
code: 63,
message: 'An unexpected error occurred',
data: 'data',
};
const method = 'GET';
const error = new RpcError(baseError, method, method);

expect(error.baseError).toBe(baseError);
expect(error.message).toMatch(/^RPC: \S+ with params \S+/);
expect(error.code).toEqual(baseError.code);
expect(error.request.method).toEqual(method);
expect(error.request.params).toEqual(method);

expect(error.isType('BLOCK_NOT_FOUND')).toBe(false);
expect(error.isType('UNEXPECTED_ERROR')).toBe(true);
});
});
24 changes: 24 additions & 0 deletions scripts/generateRpcErrorMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Processes the RPC specification error types and logs the output to simplify the generation
// of an error aggregating TS type and error code mapping object. Currently used in:
// - src/types/errors.ts
// - src/utils/errors/rpc.ts

const starknet_api_openrpc = require('starknet_specs/api/starknet_api_openrpc.json');
ivpavici marked this conversation as resolved.
Show resolved Hide resolved
const starknet_trace_api_openrpc = require('starknet_specs/api/starknet_trace_api_openrpc.json');
const starknet_write_api = require('starknet_specs/api/starknet_write_api.json');

const errorNameCodeMap = Object.fromEntries(
Object.entries({
...starknet_trace_api_openrpc.components.errors,
...starknet_write_api.components.errors,
...starknet_api_openrpc.components.errors,
})
.map((e) => [e[0], e[1].code])
.sort((a, b) => a[1] - b[1])
);

console.log('errorCodes:');
console.log(errorNameCodeMap);
console.log();
console.log('errorTypes:');
Object.keys(errorNameCodeMap).forEach((n) => console.log(`${n}: Errors.${n};`));
9 changes: 3 additions & 6 deletions src/channel/rpc_0_6.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NetworkName, StarknetChainId } from '../constants';
import { LibraryError } from '../provider/errors';
import { LibraryError, RpcError } from '../utils/errors';
import {
AccountInvocationItem,
AccountInvocations,
Expand All @@ -11,6 +11,7 @@ import {
DeployAccountContractTransaction,
Invocation,
InvocationsDetailsWithNonce,
RPC_ERROR,
RpcProviderOptions,
TransactionType,
getEstimateFeeBulkOptions,
Expand Down Expand Up @@ -102,11 +103,7 @@ export class RpcChannel {

protected errorHandler(method: string, params: any, rpcError?: JRPC.Error, otherError?: any) {
if (rpcError) {
const { code, message, data } = rpcError;
throw new LibraryError(
`RPC: ${method} with params ${stringify(params, null, 2)}\n
${code}: ${message}: ${stringify(data)}`
);
throw new RpcError(rpcError as RPC_ERROR, method, params);
}
if (otherError instanceof LibraryError) {
throw otherError;
Expand Down
9 changes: 3 additions & 6 deletions src/channel/rpc_0_7.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NetworkName, StarknetChainId } from '../constants';
import { LibraryError } from '../provider/errors';
import { LibraryError, RpcError } from '../utils/errors';
import {
AccountInvocationItem,
AccountInvocations,
Expand All @@ -11,6 +11,7 @@ import {
DeployAccountContractTransaction,
Invocation,
InvocationsDetailsWithNonce,
RPC_ERROR,
RpcProviderOptions,
TransactionType,
getEstimateFeeBulkOptions,
Expand Down Expand Up @@ -118,11 +119,7 @@ export class RpcChannel {

protected errorHandler(method: string, params: any, rpcError?: JRPC.Error, otherError?: any) {
if (rpcError) {
const { code, message, data } = rpcError;
throw new LibraryError(
`RPC: ${method} with params ${stringify(params, null, 2)}\n
${code}: ${message}: ${stringify(data)}`
);
throw new RpcError(rpcError as RPC_ERROR, method, params);
}
if (otherError instanceof LibraryError) {
throw otherError;
Expand Down
2 changes: 1 addition & 1 deletion src/provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RpcProvider } from './rpc';

export { RpcProvider as Provider } from './extensions/default'; // backward-compatibility
export * from './errors';
export * from '../utils/errors';
export * from './interface';
export * from './extensions/default';

Expand Down
2 changes: 1 addition & 1 deletion src/provider/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { RPCResponseParser } from '../utils/responseParser/rpc';
import { formatSignature } from '../utils/stark';
import { GetTransactionReceiptResponse, ReceiptTx } from '../utils/transactionReceipt';
import { getMessageHash, validateTypedData } from '../utils/typedData';
import { LibraryError } from './errors';
import { LibraryError } from '../utils/errors';
import { ProviderInterface } from './interface';

export class RpcProvider implements ProviderInterface {
Expand Down
33 changes: 33 additions & 0 deletions src/types/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Errors } from 'starknet-types-07';

// NOTE: generated with scripts/generateRpcErrorMap.js
export type RPC_ERROR_SET = {
FAILED_TO_RECEIVE_TXN: Errors.FAILED_TO_RECEIVE_TXN;
NO_TRACE_AVAILABLE: Errors.NO_TRACE_AVAILABLE;
CONTRACT_NOT_FOUND: Errors.CONTRACT_NOT_FOUND;
BLOCK_NOT_FOUND: Errors.BLOCK_NOT_FOUND;
INVALID_TXN_INDEX: Errors.INVALID_TXN_INDEX;
CLASS_HASH_NOT_FOUND: Errors.CLASS_HASH_NOT_FOUND;
TXN_HASH_NOT_FOUND: Errors.TXN_HASH_NOT_FOUND;
PAGE_SIZE_TOO_BIG: Errors.PAGE_SIZE_TOO_BIG;
NO_BLOCKS: Errors.NO_BLOCKS;
INVALID_CONTINUATION_TOKEN: Errors.INVALID_CONTINUATION_TOKEN;
TOO_MANY_KEYS_IN_FILTER: Errors.TOO_MANY_KEYS_IN_FILTER;
CONTRACT_ERROR: Errors.CONTRACT_ERROR;
TRANSACTION_EXECUTION_ERROR: Errors.TRANSACTION_EXECUTION_ERROR;
CLASS_ALREADY_DECLARED: Errors.CLASS_ALREADY_DECLARED;
INVALID_TRANSACTION_NONCE: Errors.INVALID_TRANSACTION_NONCE;
INSUFFICIENT_MAX_FEE: Errors.INSUFFICIENT_MAX_FEE;
INSUFFICIENT_ACCOUNT_BALANCE: Errors.INSUFFICIENT_ACCOUNT_BALANCE;
VALIDATION_FAILURE: Errors.VALIDATION_FAILURE;
COMPILATION_FAILED: Errors.COMPILATION_FAILED;
CONTRACT_CLASS_SIZE_IS_TOO_LARGE: Errors.CONTRACT_CLASS_SIZE_IS_TOO_LARGE;
NON_ACCOUNT: Errors.NON_ACCOUNT;
DUPLICATE_TX: Errors.DUPLICATE_TX;
COMPILED_CLASS_HASH_MISMATCH: Errors.COMPILED_CLASS_HASH_MISMATCH;
UNSUPPORTED_TX_VERSION: Errors.UNSUPPORTED_TX_VERSION;
UNSUPPORTED_CONTRACT_CLASS_VERSION: Errors.UNSUPPORTED_CONTRACT_CLASS_VERSION;
UNEXPECTED_ERROR: Errors.UNEXPECTED_ERROR;
};

export type RPC_ERROR = RPC_ERROR_SET[keyof RPC_ERROR_SET];
12 changes: 7 additions & 5 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export * from './lib';
export * from './provider';

export * from './account';
export * from './cairoEnum';
export * from './calldata';
export * from './contract';
export * from './lib';
export * from './provider';
export * from './errors';
export * from './outsideExecution';
export * from './signer';
export * from './typedData';
export * from './cairoEnum';
export * from './transactionReceipt';
export * from './outsideExecution';
export * from './typedData';

export * as RPC from './api';
45 changes: 34 additions & 11 deletions src/provider/errors.ts → src/utils/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/* eslint-disable max-classes-per-file */
import { RPC, RPC_ERROR, RPC_ERROR_SET } from '../../types';
import { stringify } from '../json';
import rpcErrors from './rpc';

// eslint-disable-next-line max-classes-per-file
export function fixStack(target: Error, fn: Function = target.constructor) {
const { captureStackTrace } = Error as any;
Expand Down Expand Up @@ -36,20 +41,38 @@ export class CustomError extends Error {

export class LibraryError extends CustomError {}

export class GatewayError extends LibraryError {
export class RpcError<BaseErrorT extends RPC_ERROR = RPC_ERROR> extends LibraryError {
public readonly request: {
method: string;
params: any;
};

constructor(
message: string,
public errorCode: string
public readonly baseError: BaseErrorT,
method: string,
params: any
) {
super(message);
// legacy message format
super(`RPC: ${method} with params ${stringify(params, null, 2)}\n
${baseError.code}: ${baseError.message}: ${stringify((baseError as RPC.JRPC.Error).data)}`);

this.request = { method, params };
}
}

export class HttpError extends LibraryError {
constructor(
message: string,
public errorCode: number
) {
super(message);
public get code() {
return this.baseError.code;
}

/**
* Verifies the underlying RPC error, also serves as a type guard for the _baseError_ property
* @example
* ```typescript
* SomeError.isType('UNEXPECTED_ERROR');
* ```
*/
public isType<N extends keyof RPC_ERROR_SET, C extends RPC_ERROR_SET[N]['code']>(
typeName: N
): this is RpcError<RPC_ERROR_SET[N] & { code: C }> {
return rpcErrors[typeName] === this.code;
}
}
32 changes: 32 additions & 0 deletions src/utils/errors/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { RPC_ERROR_SET } from '../../types';

// NOTE: generated with scripts/generateRpcErrorMap.js
const errorCodes: { [K in keyof RPC_ERROR_SET]: RPC_ERROR_SET[K]['code'] } = {
FAILED_TO_RECEIVE_TXN: 1,
NO_TRACE_AVAILABLE: 10,
CONTRACT_NOT_FOUND: 20,
BLOCK_NOT_FOUND: 24,
INVALID_TXN_INDEX: 27,
CLASS_HASH_NOT_FOUND: 28,
TXN_HASH_NOT_FOUND: 29,
PAGE_SIZE_TOO_BIG: 31,
NO_BLOCKS: 32,
INVALID_CONTINUATION_TOKEN: 33,
TOO_MANY_KEYS_IN_FILTER: 34,
CONTRACT_ERROR: 40,
TRANSACTION_EXECUTION_ERROR: 41,
CLASS_ALREADY_DECLARED: 51,
INVALID_TRANSACTION_NONCE: 52,
INSUFFICIENT_MAX_FEE: 53,
INSUFFICIENT_ACCOUNT_BALANCE: 54,
VALIDATION_FAILURE: 55,
COMPILATION_FAILED: 56,
CONTRACT_CLASS_SIZE_IS_TOO_LARGE: 57,
NON_ACCOUNT: 58,
DUPLICATE_TX: 59,
COMPILED_CLASS_HASH_MISMATCH: 60,
UNSUPPORTED_TX_VERSION: 61,
UNSUPPORTED_CONTRACT_CLASS_VERSION: 62,
UNEXPECTED_ERROR: 63,
};
export default errorCodes;
14 changes: 14 additions & 0 deletions www/docs/guides/connect_network.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,17 @@ const [getBlockResponse, blockHashAndNumber, txCount] = await Promise.all([

// ... usage of getBlockResponse, blockHashAndNumber, txCount
```

## Error handling

The [Starknet RPC specification](https://github.com/starkware-libs/starknet-specs) defines a set of possible errors that the RPC endpoints could return for various scenarios. If such errors arise `starknet.js` represents them with the corresponding [RpcError](../API/classes/RpcError) class where the endpoint error response information is contained within the `baseError` property. Also of note is that the class has an `isType` convenience method that verifies the base error type as shown in the example below.

#### Example

```typescript
try {
...
} catch (error) {
if (error instanceof RpcError && error.isType('UNEXPECTED_ERROR')) { ... }
}
```