Skip to content

Commit

Permalink
feat: increase rpc error information propagation
Browse files Browse the repository at this point in the history
  • Loading branch information
penovicp committed Sep 19, 2024
1 parent 47e52cf commit 02c54b0
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 31 deletions.
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');
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')) { ... }
}
```

0 comments on commit 02c54b0

Please sign in to comment.