Skip to content

Commit

Permalink
Replace RpcError with a coded exception (#2286)
Browse files Browse the repository at this point in the history
# Summary

So wow. I completely missed these as part of #2118, and it turns out they're a really big deal and required a ton of changes.

There are:

* Errors that have enough data to format a message
* Errors that have no data, but also no context in the message
* Errors that have no data, but really should, because you can't format a message without it
* Preflight errors in which is nested a `TransactionError` (see #2213)

In this PR we create a helper that takes in the `RpcSimulateTransactionResult` from the RPC and reformats it as a coded `SolanaError`.

As always, everything you need to know is in the `packages/errors/src/__tests__/json-rpc-error-test.ts`.

# Test Plan

```
pnpm turbo test:unit:browser
pnpm turbo test:unit:node
```
  • Loading branch information
steveluscher committed Mar 12, 2024
1 parent 34ecac6 commit 52a5d3d
Show file tree
Hide file tree
Showing 45 changed files with 957 additions and 350 deletions.
147 changes: 147 additions & 0 deletions packages/errors/src/__tests__/json-rpc-error-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR,
SOLANA_ERROR__JSON_RPC__INVALID_PARAMS,
SOLANA_ERROR__JSON_RPC__INVALID_REQUEST,
SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND,
SOLANA_ERROR__JSON_RPC__PARSE_ERROR,
SOLANA_ERROR__JSON_RPC__SCAN_ERROR,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SNAPSHOT,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION,
SolanaErrorCode,
} from '../codes';
import { SolanaErrorContext } from '../context';
import { SolanaError } from '../error';
import { getSolanaErrorFromJsonRpcError } from '../json-rpc-error';
import { getSolanaErrorFromTransactionError } from '../transaction-error';

jest.mock('../transaction-error.ts');

describe('getSolanaErrorFromJsonRpcError', () => {
it('produces a `SolanaError` with the same code as the one given', () => {
const code = 123 as SolanaErrorCode;
const error = getSolanaErrorFromJsonRpcError({ code, message: 'o no' });
expect(error).toHaveProperty('context.__code', 123);
});
describe.each([
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY,
])('given a %s JSON-RPC error known to have data', jsonRpcErrorCode => {
const expectedData = { baz: 'bat', foo: 'bar' } as unknown as SolanaErrorContext[SolanaErrorCode];
it('does not set the server message on context', () => {
const error = getSolanaErrorFromJsonRpcError({
code: jsonRpcErrorCode,
data: expectedData,
message: 'o no',
});
expect(error).not.toHaveProperty('context.__serverMessage');
});
it('produces a `SolanaError` with that data as context', () => {
const error = getSolanaErrorFromJsonRpcError({
code: jsonRpcErrorCode,
data: expectedData,
message: 'o no',
});
expect(error).toHaveProperty('context', expect.objectContaining(expectedData));
});
});
describe.each([
SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR,
SOLANA_ERROR__JSON_RPC__INVALID_PARAMS,
SOLANA_ERROR__JSON_RPC__INVALID_REQUEST,
SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND,
SOLANA_ERROR__JSON_RPC__PARSE_ERROR,
SOLANA_ERROR__JSON_RPC__SCAN_ERROR,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION,
])(
'given a %s JSON-RPC error known to have no data but important context in the server message',
jsonRpcErrorCode => {
it('produces a `SolanaError` with the server message on the context', () => {
const error = getSolanaErrorFromJsonRpcError({ code: jsonRpcErrorCode, message: 'o no' });
expect(error).toHaveProperty('context.__serverMessage', 'o no');
});
},
);
describe.each([
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SNAPSHOT,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE,
])(
'given a %s JSON-RPC error known to have neither data nor important context in the server message',
jsonRpcErrorCode => {
it('produces a `SolanaError` without the server message on the context', () => {
const error = getSolanaErrorFromJsonRpcError({ code: jsonRpcErrorCode, message: 'o no' });
expect(error).not.toHaveProperty('context.__serverMessage', 'o no');
});
},
);
describe.each([[1, 2, 3], Symbol('a symbol'), 1, 1n, true, false])('when given non-object data like `%s`', data => {
it('does not add the data to `context`', () => {
const error = getSolanaErrorFromJsonRpcError({
code: 123,
data,
message: 'o no',
});
expect(error).toHaveProperty(
'context',
// Implies exact match; `context` contains nothing but the `__code`
{ __code: 123 },
);
});
});
describe('when passed a preflight failure', () => {
it('produces a `SolanaError` with the transaction error as the `cause`', () => {
const mockErrorResult = Symbol() as unknown as SolanaError;
jest.mocked(getSolanaErrorFromTransactionError).mockReturnValue(mockErrorResult);
const error = getSolanaErrorFromJsonRpcError({
code: SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
data: { err: Symbol() },
message: 'o no',
});
expect(error.cause).toBe(mockErrorResult);
});
it('produces a `SolanaError` with the preflight failure data (minus the `err` property) as the context', () => {
const preflightErrorData = { bar: 2, baz: 3, foo: 1 };
const error = getSolanaErrorFromJsonRpcError({
code: SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
data: { ...preflightErrorData, err: Symbol() },
message: 'o no',
});
expect(error.context).toEqual({
__code: SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
...preflightErrorData,
});
});
it('delegates `err` to the transaction error getter', () => {
const transactionError = Symbol();
getSolanaErrorFromJsonRpcError({
code: SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
data: { err: transactionError },
message: 'o no',
});
expect(getSolanaErrorFromTransactionError).toHaveBeenCalledWith(transactionError);
});
});
});
46 changes: 46 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,31 @@ export const SOLANA_ERROR__MALFORMED_BIGINT_STRING = 7 as const;
export const SOLANA_ERROR__MALFORMED_NUMBER_STRING = 8 as const;
export const SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE = 9 as const;

// JSON-RPC-related errors.
// Reserve error codes in the range [-32768, -32000]
// Keep in sync with https://github.com/anza-xyz/agave/blob/master/rpc-client-api/src/custom_error.rs
export const SOLANA_ERROR__JSON_RPC__PARSE_ERROR = -32700 as const;
export const SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR = -32603 as const;
export const SOLANA_ERROR__JSON_RPC__INVALID_PARAMS = -32602 as const;
export const SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND = -32601 as const;
export const SOLANA_ERROR__JSON_RPC__INVALID_REQUEST = -32600 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED = -32016 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION = -32015 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET = -32014 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH = -32013 as const;
export const SOLANA_ERROR__JSON_RPC__SCAN_ERROR = -32012 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE = -32011 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX = -32010 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED = -32009 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SNAPSHOT = -32008 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED = -32007 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE = -32006 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY = -32005 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE = -32004 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE = -32003 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE = -32002 as const;
export const SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP = -32001 as const;

// Addresses-related errors.
// Reserve error codes in the range [2800000-2800999].
export const SOLANA_ERROR__ADDRESSES__INVALID_BYTE_LENGTH = 2800000 as const;
Expand Down Expand Up @@ -363,6 +388,27 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING
| typeof SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR
| typeof SOLANA_ERROR__JSON_RPC__INVALID_PARAMS
| typeof SOLANA_ERROR__JSON_RPC__INVALID_REQUEST
| typeof SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND
| typeof SOLANA_ERROR__JSON_RPC__PARSE_ERROR
| typeof SOLANA_ERROR__JSON_RPC__SCAN_ERROR
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SNAPSHOT
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE
| typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION
| typeof SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH
| typeof SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH
| typeof SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH
Expand Down
70 changes: 70 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,23 @@ import {
SOLANA_ERROR__INVALID_NONCE,
SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE,
SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR,
SOLANA_ERROR__JSON_RPC__INVALID_PARAMS,
SOLANA_ERROR__JSON_RPC__INVALID_REQUEST,
SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND,
SOLANA_ERROR__JSON_RPC__PARSE_ERROR,
SOLANA_ERROR__JSON_RPC__SCAN_ERROR,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION,
SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH,
SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH,
SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH,
Expand Down Expand Up @@ -120,6 +137,7 @@ import {
SOLANA_ERROR__TRANSACTION_ERROR__UNKNOWN,
SolanaErrorCode,
} from './codes';
import { RpcSimulateTransactionResult } from './json-rpc-error';

type BasicInstructionErrorContext<T extends SolanaErrorCode> = Readonly<{ [P in T]: { index: number } }>;

Expand Down Expand Up @@ -320,6 +338,58 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE]: {
unexpectedValue: unknown;
};
[SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__INVALID_PARAMS]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__INVALID_REQUEST]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__PARSE_ERROR]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SCAN_ERROR]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED]: {
contextSlot: number;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY]: {
numSlotsBehind?: number;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE]: Omit<
RpcSimulateTransactionResult,
'err'
>;
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE]: {
__serverMessage: string;
};
[SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION]: {
__serverMessage: string;
};
[SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH]: {
byteLength: number;
};
Expand Down
1 change: 1 addition & 0 deletions packages/errors/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './codes';
export * from './error';
export * from './json-rpc-error';
export * from './instruction-error';
export * from './transaction-error';
Loading

0 comments on commit 52a5d3d

Please sign in to comment.