From 52a5d3db60e702ccf77b4d17b8a3fd388e6e8584 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Tue, 12 Mar 2024 11:01:43 -0700 Subject: [PATCH] Replace `RpcError` with a coded exception (#2286) # 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 ``` --- .../src/__tests__/json-rpc-error-test.ts | 147 ++++++++++++++++ packages/errors/src/codes.ts | 46 +++++ packages/errors/src/context.ts | 70 ++++++++ packages/errors/src/index.ts | 1 + packages/errors/src/json-rpc-error.ts | 133 +++++++++++++++ packages/errors/src/messages.ts | 47 ++++++ packages/rpc-api/package.json | 1 + .../src/__tests__/get-account-info-test.ts | 18 +- .../rpc-api/src/__tests__/get-balance-test.ts | 18 +- .../rpc-api/src/__tests__/get-block-height.ts | 18 +- .../__tests__/get-block-production-test.ts | 21 ++- .../src/__tests__/get-block-time-test.ts | 14 +- .../src/__tests__/get-epoch-info-test.ts | 18 +- .../src/__tests__/get-fee-for-message-test.ts | 33 ++-- .../rpc-api/src/__tests__/get-health-test.ts | 18 +- .../__tests__/get-inflation-reward-test.ts | 39 +++-- .../__tests__/get-latest-blockhash-test.ts | 18 +- .../__tests__/get-multiple-accounts-test.ts | 18 +- .../__tests__/get-program-accounts-test.ts | 18 +- .../__tests__/get-signature-statuses-test.ts | 14 +- .../get-signatures-for-address-test.ts | 17 +- .../src/__tests__/get-slot-leader-test.ts | 18 +- .../src/__tests__/get-slot-leaders-test.ts | 25 +-- .../rpc-api/src/__tests__/get-slot-test.ts | 18 +- .../__tests__/get-stake-activation-test.ts | 45 +++-- .../get-token-account-balance-test.ts | 15 +- .../get-token-accounts-by-delegate-test.ts | 44 +++-- .../get-token-accounts-by-owner-test.ts | 44 +++-- .../get-token-largest-accounts-test.ts | 15 +- .../src/__tests__/get-token-supply-test.ts | 15 +- .../__tests__/get-transaction-count-test.ts | 18 +- .../src/__tests__/send-transaction-test.ts | 158 +++++++++++++----- .../__tests__/simulate-transaction-test.ts | 62 ++++--- packages/rpc-spec-types/src/index.ts | 1 - packages/rpc-spec-types/src/rpc-error.ts | 19 --- packages/rpc-spec-types/src/rpc-response.ts | 8 +- packages/rpc-spec/package.json | 1 + packages/rpc-spec/src/__tests__/rpc-test.ts | 15 +- packages/rpc-spec/src/rpc.ts | 4 +- .../src/__tests__/rpc-subscription-test.ts | 11 +- .../src/rpc-subscriptions.ts | 4 +- packages/rpc-types/src/index.ts | 1 - packages/rpc-types/src/rpc-errors.ts | 23 --- pnpm-lock.yaml | 6 + ...nsaction-fee-payer-insufficient-funds.json | 10 ++ 45 files changed, 957 insertions(+), 350 deletions(-) create mode 100644 packages/errors/src/__tests__/json-rpc-error-test.ts create mode 100644 packages/errors/src/json-rpc-error.ts delete mode 100644 packages/rpc-spec-types/src/rpc-error.ts delete mode 100644 packages/rpc-types/src/rpc-errors.ts create mode 100644 scripts/fixtures/send-transaction-fee-payer-insufficient-funds.json diff --git a/packages/errors/src/__tests__/json-rpc-error-test.ts b/packages/errors/src/__tests__/json-rpc-error-test.ts new file mode 100644 index 000000000000..f26a022357b9 --- /dev/null +++ b/packages/errors/src/__tests__/json-rpc-error-test.ts @@ -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); + }); + }); +}); diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 5e4e9b514797..cabce24721d8 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -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; @@ -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 diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index d58430339d3b..aed2f5f8a54a 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -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, @@ -120,6 +137,7 @@ import { SOLANA_ERROR__TRANSACTION_ERROR__UNKNOWN, SolanaErrorCode, } from './codes'; +import { RpcSimulateTransactionResult } from './json-rpc-error'; type BasicInstructionErrorContext = Readonly<{ [P in T]: { index: number } }>; @@ -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; }; diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index bba62edff7a4..bf5f65366b0e 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -1,4 +1,5 @@ export * from './codes'; export * from './error'; +export * from './json-rpc-error'; export * from './instruction-error'; export * from './transaction-error'; diff --git a/packages/errors/src/json-rpc-error.ts b/packages/errors/src/json-rpc-error.ts new file mode 100644 index 000000000000..729d55b1a039 --- /dev/null +++ b/packages/errors/src/json-rpc-error.ts @@ -0,0 +1,133 @@ +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_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, + SolanaErrorCode, +} from './codes'; +import { SolanaErrorContext } from './context'; +import { SolanaError } from './error'; +import { getSolanaErrorFromTransactionError } from './transaction-error'; + +interface RpcErrorResponse { + code: number; + data?: unknown; + message: string; +} + +type TransactionError = string | { [key: string]: unknown }; + +// Keep in sync with https://github.com/anza-xyz/agave/blob/master/rpc-client-api/src/response.rs +export interface RpcSimulateTransactionResult { + accounts: + | ({ + data: + | string // LegacyBinary + | { + // Json + parsed: unknown; + program: string; + space: number; + } + // Binary + | [encodedBytes: string, encoding: 'base58' | 'base64' | 'base64+zstd' | 'binary' | 'jsonParsed']; + executable: boolean; + lamports: number; + owner: string; + rentEpoch: number; + space?: number; + } | null)[] + | null; + err: TransactionError | null; + // Enabled by `enable_cpi_recording` + innerInstructions?: + | { + index: number; + instructions: ( + | { + // Compiled + accounts: number[]; + data: string; + programIdIndex: number; + stack_height?: number; + } + | { + // Parsed + parsed: unknown; + program: string; + program_id: string; + stack_height?: number; + } + | { + // PartiallyDecoded + accounts: string[]; + data: string; + program_id: string; + stack_height?: number; + } + )[]; + }[] + | null; + logs: string[] | null; + returnData: { + data: [string, 'base64']; + programId: string; + } | null; + unitsConsumed: number | null; +} + +export function getSolanaErrorFromJsonRpcError({ code, data, message }: RpcErrorResponse): SolanaError { + let out: SolanaError; + if (code === SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE) { + const { err, ...preflightErrorContext } = data as RpcSimulateTransactionResult; + const causeObject = err ? { cause: getSolanaErrorFromTransactionError(err) } : null; + out = new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, { + ...preflightErrorContext, + ...causeObject, + }); + } else { + let errorContext; + switch (code) { + case SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR: + case SOLANA_ERROR__JSON_RPC__INVALID_PARAMS: + case SOLANA_ERROR__JSON_RPC__INVALID_REQUEST: + case SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND: + case SOLANA_ERROR__JSON_RPC__PARSE_ERROR: + case SOLANA_ERROR__JSON_RPC__SCAN_ERROR: + case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP: + case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE: + case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET: + case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX: + case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED: + case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED: + case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE: + case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION: + // The server supplies no structured data, but rather a pre-formatted message. Put + // the server message in `context` so as not to completely lose the data. The long + // term fix for this is to add data to the server responses and modify the + // messages in `@solana/errors` to be actual format strings. + errorContext = { __serverMessage: message }; + break; + default: + if (typeof data === 'object' && !Array.isArray(data)) { + errorContext = data; + } + } + out = new SolanaError(code as SolanaErrorCode, errorContext as SolanaErrorContext[SolanaErrorCode]); + } + if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(out, getSolanaErrorFromJsonRpcError); + } + return out; +} diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 424f423e05e7..0904c0708b84 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -94,6 +94,27 @@ import { SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE, SOLANA_ERROR__INVARIANT_VIOLATION__WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE, SOLANA_ERROR__INVARIANT_VIOLATION__WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING, + 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, SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH, SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH, @@ -342,6 +363,32 @@ export const SolanaErrorMessages: Readonly<{ [SOLANA_ERROR__INVARIANT_VIOLATION__WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING]: 'Invariant violation: WebSocket message iterator is missing state storage. It should be ' + 'impossible to hit this error; please file an issue at https://sola.na/web3invariant', + [SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR]: 'JSON-RPC error: Internal JSON-RPC error ($__serverMessage)', + [SOLANA_ERROR__JSON_RPC__INVALID_PARAMS]: 'JSON-RPC error: Invalid method parameter(s) ($__serverMessage)', + [SOLANA_ERROR__JSON_RPC__INVALID_REQUEST]: + 'JSON-RPC error: The JSON sent is not a valid `Request` object ($__serverMessage)', + [SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND]: + 'JSON-RPC error: The method does not exist / is not available ($__serverMessage)', + [SOLANA_ERROR__JSON_RPC__PARSE_ERROR]: + 'JSON-RPC error: An error occurred on the server while parsing the JSON text ($__serverMessage)', + [SOLANA_ERROR__JSON_RPC__SCAN_ERROR]: '$__serverMessage', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP]: '$__serverMessage', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE]: '$__serverMessage', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET]: '$__serverMessage', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX]: '$__serverMessage', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED]: '$__serverMessage', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED]: 'Minimum context slot has not been reached', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY]: 'Node is unhealthy; behind by $numSlotsBehind slots', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SNAPSHOT]: 'No snapshot', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE]: 'Transaction simulation failed', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED]: '$__serverMessage', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE]: + 'Transaction history is not available from this node', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE]: '$__serverMessage', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH]: 'Transaction signature length mismatch', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE]: + 'Transaction signature verification failure', + [SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION]: '$__serverMessage', [SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH]: 'Key pair bytes must be of length 64, got $byteLength.', [SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH]: 'Expected private key bytes with length 32. Actual length: $actualLength.', diff --git a/packages/rpc-api/package.json b/packages/rpc-api/package.json index 0446624780a9..c69bb699dd2f 100644 --- a/packages/rpc-api/package.json +++ b/packages/rpc-api/package.json @@ -66,6 +66,7 @@ "@solana/addresses": "workspace:*", "@solana/codecs-core": "workspace:*", "@solana/codecs-strings": "workspace:*", + "@solana/errors": "workspace:*", "@solana/keys": "workspace:*", "@solana/rpc-spec": "workspace:*", "@solana/rpc-parsed-types": "workspace:*", diff --git a/packages/rpc-api/src/__tests__/get-account-info-test.ts b/packages/rpc-api/src/__tests__/get-account-info-test.ts index 797bb186d134..01b26852ad22 100644 --- a/packages/rpc-api/src/__tests__/get-account-info-test.ts +++ b/packages/rpc-api/src/__tests__/get-account-info-test.ts @@ -1,7 +1,7 @@ import type { Address } from '@solana/addresses'; +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetAccountInfoApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -47,7 +47,7 @@ describe('getAccountInfo', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const publicKey = 'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G' as Address<'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G'>; const sendPromise = rpc @@ -55,10 +55,14 @@ describe('getAccountInfo', () => { minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); diff --git a/packages/rpc-api/src/__tests__/get-balance-test.ts b/packages/rpc-api/src/__tests__/get-balance-test.ts index 05e36e453643..9d5f179e2ecd 100644 --- a/packages/rpc-api/src/__tests__/get-balance-test.ts +++ b/packages/rpc-api/src/__tests__/get-balance-test.ts @@ -1,7 +1,7 @@ import type { Address } from '@solana/addresses'; +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetBalanceApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -38,7 +38,7 @@ describe('getBalance', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); // This key is random, don't re-use in any tests that affect balance const publicKey = '4BfxgLzn6pEuVB2ynBMqckHFdYD8VNcrheDFFCB6U5TH' as Address<'4BfxgLzn6pEuVB2ynBMqckHFdYD8VNcrheDFFCB6U5TH'>; @@ -47,10 +47,14 @@ describe('getBalance', () => { minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-block-height.ts b/packages/rpc-api/src/__tests__/get-block-height.ts index 017aae997866..58ebb691e76c 100644 --- a/packages/rpc-api/src/__tests__/get-block-height.ts +++ b/packages/rpc-api/src/__tests__/get-block-height.ts @@ -1,6 +1,6 @@ +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetBlockHeightApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -28,16 +28,20 @@ describe('getBlockHeight', () => { }); describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const sendPromise = rpc .getBlockHeight({ minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-block-production-test.ts b/packages/rpc-api/src/__tests__/get-block-production-test.ts index f679c8314bd5..249b0f3ee2f7 100644 --- a/packages/rpc-api/src/__tests__/get-block-production-test.ts +++ b/packages/rpc-api/src/__tests__/get-block-production-test.ts @@ -1,7 +1,7 @@ import type { Address } from '@solana/addresses'; +import { SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetBlockProductionApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -62,7 +62,7 @@ describe('getBlockProduction', () => { describe('when called with a `lastSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const blockProductionPromise = rpc .getBlockProduction({ range: { @@ -71,10 +71,17 @@ describe('getBlockProduction', () => { }, }) .send(); - await expect(blockProductionPromise).rejects.toThrow(RpcError); - await expect(blockProductionPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await Promise.all([ + expect(blockProductionPromise).rejects.toThrow(SolanaError), + expect(blockProductionPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, + ), + expect(blockProductionPromise).rejects.toHaveProperty( + 'context.__serverMessage', + expect.stringMatching(/lastSlot, 9223372036854776000, is too large; max \d+/), + ), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-block-time-test.ts b/packages/rpc-api/src/__tests__/get-block-time-test.ts index 2758a911be0f..2158089d9517 100644 --- a/packages/rpc-api/src/__tests__/get-block-time-test.ts +++ b/packages/rpc-api/src/__tests__/get-block-time-test.ts @@ -1,6 +1,5 @@ +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { SolanaRpcErrorCode } from '@solana/rpc-types'; import { GetBlockTimeApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -30,13 +29,14 @@ describe('getBlockTime', () => { describe('when called with a block higher than the highest block available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const blockNumber = 2n ** 63n - 1n; // u64:MAX; safe bet it'll be too high. const blockTimePromise = rpc.getBlockTime(blockNumber).send(); - await expect(blockTimePromise).rejects.toThrow(RpcError); - await expect(blockTimePromise).rejects.toMatchObject({ - code: -32004 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE'], - }); + await expect(blockTimePromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE, { + __serverMessage: 'Block not available for slot 9223372036854776000', + }), + ); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-epoch-info-test.ts b/packages/rpc-api/src/__tests__/get-epoch-info-test.ts index 1bb6391ca581..6c6efcafcb41 100644 --- a/packages/rpc-api/src/__tests__/get-epoch-info-test.ts +++ b/packages/rpc-api/src/__tests__/get-epoch-info-test.ts @@ -1,6 +1,6 @@ +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetEpochInfoApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -30,16 +30,20 @@ describe('getEpochInfo', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const epochInfoPromise = rpc .getEpochInfo({ minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(epochInfoPromise).rejects.toThrow(RpcError); - await expect(epochInfoPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(epochInfoPromise).rejects.toThrow(SolanaError), + expect(epochInfoPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(epochInfoPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-fee-for-message-test.ts b/packages/rpc-api/src/__tests__/get-fee-for-message-test.ts index 175f305e47d9..f3f979fb2e2b 100644 --- a/packages/rpc-api/src/__tests__/get-fee-for-message-test.ts +++ b/packages/rpc-api/src/__tests__/get-fee-for-message-test.ts @@ -1,8 +1,12 @@ import { fixEncoder } from '@solana/codecs-core'; import { getBase58Encoder, getBase64Decoder } from '@solana/codecs-strings'; +import { + SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + SolanaError, +} from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Blockhash, Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Blockhash, Commitment } from '@solana/rpc-types'; import type { SerializedMessageBytesBase64 } from '@solana/transactions'; import { GetFeeForMessageApi, GetLatestBlockhashApi } from '../index'; @@ -106,18 +110,19 @@ describe('getFeeForMessage', () => { describe('when called with an invalid message', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const sendPromise = rpc.getFeeForMessage('someInvalidMessage' as SerializedMessageBytesBase64).send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'invalid base64 encoding: InvalidPadding', + }), + ); }); }); describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const latestBlockhash = await rpc.getLatestBlockhash().send(); const message = getMockTransactionMessage(latestBlockhash.value.blockhash); const sendPromise = rpc @@ -125,10 +130,14 @@ describe('getFeeForMessage', () => { minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-health-test.ts b/packages/rpc-api/src/__tests__/get-health-test.ts index 665843beea8c..e93b3614f051 100644 --- a/packages/rpc-api/src/__tests__/get-health-test.ts +++ b/packages/rpc-api/src/__tests__/get-health-test.ts @@ -1,5 +1,5 @@ +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY, SolanaError } from '@solana/errors'; import { createRpc, type Rpc } from '@solana/rpc-spec'; -import type { SolanaRpcErrorCode } from '@solana/rpc-types'; import { createSolanaRpcApi, GetHealthApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -20,26 +20,26 @@ describe('getHealth', () => { describe('when the node is unhealthy', () => { let rpc: Rpc; const errorMessage = 'Node is unhealthy'; - const errorCode = -32005; + const errorCode = SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY; const errorObject = { code: errorCode, + data: { numSlotsBehind: 123 }, message: errorMessage, - name: 'RpcError', }; beforeEach(() => { rpc = createRpc({ api: createSolanaRpcApi(), - transport: jest.fn().mockRejectedValue(errorObject), + transport: jest.fn().mockResolvedValue({ error: errorObject }), }); }); it('returns an error message', async () => { expect.assertions(1); const healthPromise = rpc.getHealth().send(); - await expect(healthPromise).rejects.toMatchObject({ - code: errorCode satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY'], - message: errorMessage, - name: 'RpcError', - }); + await expect(healthPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY, { + numSlotsBehind: 123, + }), + ); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-inflation-reward-test.ts b/packages/rpc-api/src/__tests__/get-inflation-reward-test.ts index da66a36f6a9d..7f46c1c6d56f 100644 --- a/packages/rpc-api/src/__tests__/get-inflation-reward-test.ts +++ b/packages/rpc-api/src/__tests__/get-inflation-reward-test.ts @@ -1,6 +1,10 @@ +import { + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + SolanaError, +} from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetInflationRewardApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -27,30 +31,41 @@ describe('getInflationReward', () => { }); describe('when called with an `epoch` higher than the highest epoch available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const sendPromise = rpc .getInflationReward([], { epoch: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32004 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE, + ), + expect(sendPromise).rejects.toHaveProperty( + 'context.__serverMessage', + expect.stringMatching(/Block not available for slot \d+/), + ), + ]); }); }); describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const sendPromise = rpc .getInflationReward([], { minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-latest-blockhash-test.ts b/packages/rpc-api/src/__tests__/get-latest-blockhash-test.ts index 651194cbf80c..9bb29cd2a384 100644 --- a/packages/rpc-api/src/__tests__/get-latest-blockhash-test.ts +++ b/packages/rpc-api/src/__tests__/get-latest-blockhash-test.ts @@ -1,6 +1,6 @@ +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetLatestBlockhashApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -33,16 +33,20 @@ describe('getLatestBlockhash', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const sendPromise = rpc .getLatestBlockhash({ minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts b/packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts index 525f0d29409e..438cd696404e 100644 --- a/packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts +++ b/packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts @@ -1,7 +1,7 @@ import type { Address } from '@solana/addresses'; +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetMultipleAccountsApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -62,7 +62,7 @@ describe('getMultipleAccounts', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const publicKey = 'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G' as Address<'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G'>; const sendPromise = rpc @@ -70,10 +70,14 @@ describe('getMultipleAccounts', () => { minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); diff --git a/packages/rpc-api/src/__tests__/get-program-accounts-test.ts b/packages/rpc-api/src/__tests__/get-program-accounts-test.ts index c48b134b19db..1e73a276bbf3 100644 --- a/packages/rpc-api/src/__tests__/get-program-accounts-test.ts +++ b/packages/rpc-api/src/__tests__/get-program-accounts-test.ts @@ -2,9 +2,9 @@ import { open } from 'node:fs/promises'; import type { Address } from '@solana/addresses'; import { getBase58Decoder } from '@solana/codecs-strings'; +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import path from 'path'; import { GetProgramAccountsApi } from '../index'; @@ -78,7 +78,7 @@ describe('getProgramAccounts', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const program = 'DXngmJfjurhnAwbMPgpUGPH6qNvetCKRJ6PiD4ag4PTj' as Address<'DXngmJfjurhnAwbMPgpUGPH6qNvetCKRJ6PiD4ag4PTj'>; const sendPromise = rpc @@ -86,10 +86,14 @@ describe('getProgramAccounts', () => { minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); diff --git a/packages/rpc-api/src/__tests__/get-signature-statuses-test.ts b/packages/rpc-api/src/__tests__/get-signature-statuses-test.ts index 8b066a6338b2..59e177965c49 100644 --- a/packages/rpc-api/src/__tests__/get-signature-statuses-test.ts +++ b/packages/rpc-api/src/__tests__/get-signature-statuses-test.ts @@ -1,7 +1,6 @@ +import { SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, SolanaError } from '@solana/errors'; import type { Signature } from '@solana/keys'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { SolanaRpcErrorCode } from '@solana/rpc-types'; import { GetSignatureStatusesApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -45,12 +44,13 @@ describe('getSignatureStatuses', () => { describe('when called with an invalid transaction signature', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const signatureStatusPromise = rpc.getSignatureStatuses(['invalid_signature' as Signature]).send(); - await expect(signatureStatusPromise).rejects.toThrow(RpcError); - await expect(signatureStatusPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(signatureStatusPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid param: Invalid', + }), + ); }); }); diff --git a/packages/rpc-api/src/__tests__/get-signatures-for-address-test.ts b/packages/rpc-api/src/__tests__/get-signatures-for-address-test.ts index 6d3ad877a1c6..7bc60f818574 100644 --- a/packages/rpc-api/src/__tests__/get-signatures-for-address-test.ts +++ b/packages/rpc-api/src/__tests__/get-signatures-for-address-test.ts @@ -1,7 +1,6 @@ import type { Address } from '@solana/addresses'; +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { SolanaRpcErrorCode } from '@solana/rpc-types'; import { GetSignaturesForAddressApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -39,7 +38,7 @@ describe('getSignaturesForAddress', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); // This key is random, don't re-use in any tests that perform transactions const publicKey = '3F6rba4VRgdGeYzgCNWQaEJUerUEQVVuwKrETigvHhJP' as Address; const sendPromise = rpc @@ -47,10 +46,14 @@ describe('getSignaturesForAddress', () => { minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-slot-leader-test.ts b/packages/rpc-api/src/__tests__/get-slot-leader-test.ts index b4d4491988b1..59a9ac8410f9 100644 --- a/packages/rpc-api/src/__tests__/get-slot-leader-test.ts +++ b/packages/rpc-api/src/__tests__/get-slot-leader-test.ts @@ -2,9 +2,9 @@ import { open } from 'node:fs/promises'; import type { Address } from '@solana/addresses'; import { getBase58Decoder } from '@solana/codecs-strings'; +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import path from 'path'; import { GetSlotLeaderApi } from '../index'; @@ -61,16 +61,20 @@ describe('getSlotLeader', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const sendPromise = rpc .getSlotLeader({ minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-slot-leaders-test.ts b/packages/rpc-api/src/__tests__/get-slot-leaders-test.ts index 73ea063a573e..caa6b10972f1 100644 --- a/packages/rpc-api/src/__tests__/get-slot-leaders-test.ts +++ b/packages/rpc-api/src/__tests__/get-slot-leaders-test.ts @@ -2,9 +2,8 @@ import { open } from 'node:fs/promises'; import type { Address } from '@solana/addresses'; import { getBase58Decoder } from '@solana/codecs-strings'; +import { SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { SolanaRpcErrorCode } from '@solana/rpc-types'; import path from 'path'; import { GetSlotLeadersApi } from '../index'; @@ -51,30 +50,32 @@ describe('getSlotLeaders', () => { describe('when called with a `startSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const sendPromise = rpc .getSlotLeaders( 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. 3, ) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid slot range: leader schedule for epoch 21350398233460 is unavailable', + }), + ); }); }); describe('when called with a `limit` greater than 5000', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const minimumLedgerSlot = (await rpc.minimumLedgerSlot().send()) as bigint; const sendPromise = rpc.getSlotLeaders(minimumLedgerSlot, 5001).send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid limit; max 5000', + }), + ); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-slot-test.ts b/packages/rpc-api/src/__tests__/get-slot-test.ts index 7c9700f06add..bb330279a7f0 100644 --- a/packages/rpc-api/src/__tests__/get-slot-test.ts +++ b/packages/rpc-api/src/__tests__/get-slot-test.ts @@ -1,6 +1,6 @@ +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetSlotApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -28,16 +28,20 @@ describe('getSlot', () => { }); describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const sendPromise = rpc .getSlot({ minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-stake-activation-test.ts b/packages/rpc-api/src/__tests__/get-stake-activation-test.ts index 67087a626b6f..9a0010803ced 100644 --- a/packages/rpc-api/src/__tests__/get-stake-activation-test.ts +++ b/packages/rpc-api/src/__tests__/get-stake-activation-test.ts @@ -1,7 +1,11 @@ import type { Address } from '@solana/addresses'; +import { + SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + SolanaError, +} from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetStakeActivationApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -31,47 +35,54 @@ describe('getStakeActivation', () => { describe('when called with an account that is not a stake account', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const stakeActivationPromise = rpc .getStakeActivation( // Randomly generated 'BnWCFuxmi6uH3ceVx4R8qcbWBMPVVYVVFWtAiiTA1PAu' as Address, ) .send(); - await expect(stakeActivationPromise).rejects.toThrow(RpcError); - await expect(stakeActivationPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(stakeActivationPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid param: account not found', + }), + ); }); }); describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const stakeActivationPromise = rpc .getStakeActivation(stakeAccountAddress, { minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(stakeActivationPromise).rejects.toThrow(RpcError); - await expect(stakeActivationPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(stakeActivationPromise).rejects.toThrow(SolanaError), + expect(stakeActivationPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(stakeActivationPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); describe('when called with an `epoch` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const stakeActivationPromise = rpc .getStakeActivation(stakeAccountAddress, { epoch: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(stakeActivationPromise).rejects.toThrow(RpcError); - await expect(stakeActivationPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(stakeActivationPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: + 'Invalid param: epoch 9223372036854776000. Only the current epoch (0) is supported', + }), + ); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-token-account-balance-test.ts b/packages/rpc-api/src/__tests__/get-token-account-balance-test.ts index ab8bd00e5fae..a1734f708737 100644 --- a/packages/rpc-api/src/__tests__/get-token-account-balance-test.ts +++ b/packages/rpc-api/src/__tests__/get-token-account-balance-test.ts @@ -1,7 +1,7 @@ import type { Address } from '@solana/addresses'; +import { SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetTokenAccountBalanceApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -39,17 +39,18 @@ describe('getTokenAccountBalance', () => { describe('when called with an account that is not a token account', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const sendPromise = rpc .getTokenAccountBalance( // Randomly generated 'BnWCFuxmi6uH3ceVx4R8qcbWBMPVVYVVFWtAiiTA1PAu' as Address, ) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid params: missing field `commitment`.', + }), + ); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-token-accounts-by-delegate-test.ts b/packages/rpc-api/src/__tests__/get-token-accounts-by-delegate-test.ts index 1ef0660583da..773906858169 100644 --- a/packages/rpc-api/src/__tests__/get-token-accounts-by-delegate-test.ts +++ b/packages/rpc-api/src/__tests__/get-token-accounts-by-delegate-test.ts @@ -1,7 +1,11 @@ import type { Address } from '@solana/addresses'; +import { + SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + SolanaError, +} from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetTokenAccountsByDelegateApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -64,7 +68,7 @@ describe('getTokenAccountsByDelegate', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws a slot not reached error', async () => { - expect.assertions(2); + expect.assertions(3); // Delegate for fixtures/spl-token-token-account-delegated.json const delegate = 'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL' as Address<'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL'>; @@ -82,10 +86,14 @@ describe('getTokenAccountsByDelegate', () => { }, ) .send(); - await expect(accountInfoPromise).rejects.toThrow(RpcError); - await expect(accountInfoPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(accountInfoPromise).rejects.toThrow(SolanaError), + expect(accountInfoPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(accountInfoPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); @@ -109,7 +117,7 @@ describe('getTokenAccountsByDelegate', () => { describe('when called with a mint that does not exist', () => { it('throws an error for mint not existing', async () => { - expect.assertions(2); + expect.assertions(1); // Delegate for fixtures/spl-token-token-account-delegated.json const delegate = 'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL' as Address<'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL'>; @@ -119,10 +127,11 @@ describe('getTokenAccountsByDelegate', () => { 'bYaTaiLtMmTfqZaVbo2rwQrdj1iA2DdjMrSACLCSZj4' as Address<'bYaTaiLtMmTfqZaVbo2rwQrdj1iA2DdjMrSACLCSZj4'>; const accountInfoPromise = rpc.getTokenAccountsByDelegate(delegate, { mint }).send(); - await expect(accountInfoPromise).rejects.toThrow(RpcError); - await expect(accountInfoPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(accountInfoPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid param: could not find mint', + }), + ); }); }); @@ -191,7 +200,7 @@ describe('getTokenAccountsByDelegate', () => { describe('when called with a program that is not a Token program', () => { it('throws an error for unrecognized program', async () => { - expect.assertions(2); + expect.assertions(1); // Delegate for fixtures/spl-token-token-account-delegated.json const delegate = 'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL' as Address<'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL'>; @@ -201,10 +210,11 @@ describe('getTokenAccountsByDelegate', () => { 'HFnhCvdV9yyShcFNQvdR5LazsKXpnJNxwoRKjE9V1LrF' as Address<'HFnhCvdV9yyShcFNQvdR5LazsKXpnJNxwoRKjE9V1LrF'>; const accountInfoPromise = rpc.getTokenAccountsByDelegate(delegate, { programId }).send(); - await expect(accountInfoPromise).rejects.toThrow(RpcError); - await expect(accountInfoPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(accountInfoPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid param: unrecognized Token program id', + }), + ); }); }); diff --git a/packages/rpc-api/src/__tests__/get-token-accounts-by-owner-test.ts b/packages/rpc-api/src/__tests__/get-token-accounts-by-owner-test.ts index 5cdc53ba14fc..82604d14d347 100644 --- a/packages/rpc-api/src/__tests__/get-token-accounts-by-owner-test.ts +++ b/packages/rpc-api/src/__tests__/get-token-accounts-by-owner-test.ts @@ -1,7 +1,11 @@ import type { Address } from '@solana/addresses'; +import { + SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + SolanaError, +} from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetTokenAccountsByOwnerApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -64,7 +68,7 @@ describe('getTokenAccountsByOwner', () => { describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws a slot not reached error', async () => { - expect.assertions(2); + expect.assertions(3); // Owner for fixtures/spl-token-token-account-owner.json const owner = 'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL' as Address<'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL'>; @@ -82,10 +86,14 @@ describe('getTokenAccountsByOwner', () => { }, ) .send(); - await expect(accountInfoPromise).rejects.toThrow(RpcError); - await expect(accountInfoPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(accountInfoPromise).rejects.toThrow(SolanaError), + expect(accountInfoPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(accountInfoPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); @@ -109,7 +117,7 @@ describe('getTokenAccountsByOwner', () => { describe('when called with a mint that does not exist', () => { it('throws an error for mint not existing', async () => { - expect.assertions(2); + expect.assertions(1); // Owner for fixtures/spl-token-token-account-owner.json const owner = 'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL' as Address<'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL'>; @@ -119,10 +127,11 @@ describe('getTokenAccountsByOwner', () => { 'bYaTaiLtMmTfqZaVbo2rwQrdj1iA2DdjMrSACLCSZj4' as Address<'bYaTaiLtMmTfqZaVbo2rwQrdj1iA2DdjMrSACLCSZj4'>; const accountInfoPromise = rpc.getTokenAccountsByOwner(owner, { mint }).send(); - await expect(accountInfoPromise).rejects.toThrow(RpcError); - await expect(accountInfoPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(accountInfoPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid param: could not find mint', + }), + ); }); }); @@ -191,7 +200,7 @@ describe('getTokenAccountsByOwner', () => { describe('when called with a program that is not a Token program', () => { it('throws an error for unrecognized program', async () => { - expect.assertions(2); + expect.assertions(1); // Owner for fixtures/spl-token-token-account-owner.json const owner = 'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL' as Address<'G4QJANEpvEN8vLaaMZoWwZtqHfWxuWpd5RrVVYXPCgeL'>; @@ -201,10 +210,11 @@ describe('getTokenAccountsByOwner', () => { 'AfFRmCFz8yUWzug2jiRc13xEEzBwyxxYSRGVE5uQMpHk' as Address<'AfFRmCFz8yUWzug2jiRc13xEEzBwyxxYSRGVE5uQMpHk'>; const accountInfoPromise = rpc.getTokenAccountsByOwner(owner, { programId }).send(); - await expect(accountInfoPromise).rejects.toThrow(RpcError); - await expect(accountInfoPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(accountInfoPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid param: unrecognized Token program id', + }), + ); }); }); diff --git a/packages/rpc-api/src/__tests__/get-token-largest-accounts-test.ts b/packages/rpc-api/src/__tests__/get-token-largest-accounts-test.ts index 33cea83f90c1..360bb73075af 100644 --- a/packages/rpc-api/src/__tests__/get-token-largest-accounts-test.ts +++ b/packages/rpc-api/src/__tests__/get-token-largest-accounts-test.ts @@ -1,7 +1,7 @@ import type { Address } from '@solana/addresses'; +import { SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetTokenLargestAccountsApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -44,17 +44,18 @@ describe('getTokenLargestAccounts', () => { describe('when called with an account that is not a token mint', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const sendPromise = rpc .getTokenSupply( // Randomly generated 'BnWCFuxmi6uH3ceVx4R8qcbWBMPVVYVVFWtAiiTA1PAu' as Address, ) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid params: missing field `commitment`.', + }), + ); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-token-supply-test.ts b/packages/rpc-api/src/__tests__/get-token-supply-test.ts index 88528e19c090..02ab7438801a 100644 --- a/packages/rpc-api/src/__tests__/get-token-supply-test.ts +++ b/packages/rpc-api/src/__tests__/get-token-supply-test.ts @@ -1,7 +1,7 @@ import type { Address } from '@solana/addresses'; +import { SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetTokenSupplyApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -40,17 +40,18 @@ describe('getTokenSupply', () => { describe('when called with an account that is not a token mint', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(1); const sendPromise = rpc .getTokenSupply( // Randomly generated 'BnWCFuxmi6uH3ceVx4R8qcbWBMPVVYVVFWtAiiTA1PAu' as Address, ) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: 'Invalid params: missing field `commitment`.', + }), + ); }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-transaction-count-test.ts b/packages/rpc-api/src/__tests__/get-transaction-count-test.ts index 90b968d59537..5decb9925767 100644 --- a/packages/rpc-api/src/__tests__/get-transaction-count-test.ts +++ b/packages/rpc-api/src/__tests__/get-transaction-count-test.ts @@ -1,6 +1,6 @@ +import { SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SolanaError } from '@solana/errors'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import { GetTransactionCountApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; @@ -28,16 +28,20 @@ describe('getTransactionCount', () => { }); describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const sendPromise = rpc .getTransactionCount({ minContextSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. }) .send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(sendPromise).rejects.toThrow(SolanaError), + expect(sendPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(sendPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/send-transaction-test.ts b/packages/rpc-api/src/__tests__/send-transaction-test.ts index 3abbb94b4686..602c2a35d0dc 100644 --- a/packages/rpc-api/src/__tests__/send-transaction-test.ts +++ b/packages/rpc-api/src/__tests__/send-transaction-test.ts @@ -2,13 +2,22 @@ import { Buffer } from 'node:buffer'; import { fixEncoder } from '@solana/codecs-core'; import { getBase58Decoder, getBase58Encoder } from '@solana/codecs-strings'; +import { + SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE, + SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_NOT_FOUND, + SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND, + SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE, + SolanaError, +} from '@solana/errors'; import { createPrivateKeyFromBytes } from '@solana/keys'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Commitment } from '@solana/rpc-types'; import type { Base64EncodedWireTransaction } from '@solana/transactions'; -import { GetLatestBlockhashApi, SendTransactionApi } from '../index'; +import { GetLatestBlockhashApi, GetMinimumBalanceForRentExemptionApi, SendTransactionApi } from '../index'; import { createLocalhostSolanaRpc } from './__setup__'; function getMockTransactionMessage({ @@ -66,12 +75,24 @@ const MOCK_PUBLIC_KEY_BYTES = // DRtXHDgC312wpNdNCSb8vCoXDcofCJcPHdAw4VkJ8L9i 0x42, 0xfa, 0x3b, 0xb8, 0x25, 0xf0, 0xec, 0xfc, 0xe2, 0x27, 0x4d, 0x7d, 0xad, 0xad, 0x51, 0x2d, ]); -async function getSecretKey() { - return await createPrivateKeyFromBytes(MOCK_PRIVATE_KEY_BYTES, /* extractable */ false); +const MOCK_INSUFFICIENT_BALANCE_PRIVATE_KEY_BYTES = new Uint8Array([ + 153, 77, 119, 0, 167, 108, 113, 105, 100, 122, 229, 212, 244, 214, 192, 210, 79, 109, 245, 95, 24, 121, 235, 17, 55, + 166, 132, 117, 31, 134, 31, 171, +]); +// See scripts/fixtures/send-transaction-fee-payer-insuffcient-funds.json +const MOCK_INSUFFICIENT_BALANCE_PUBLIC_KEY_BYTES = // 6Zs91PMyhqyMgNVuT8EnM6f3YjVAVabvVWLjpFSC288q + // prettier-ignore + new Uint8Array([ + 0x52, 0xb5, 0xb6, 0x37, 0xf1, 0x28, 0x73, 0x8d, 0x25, 0x61, 0x59, 0xba, 0xca, 0x2b, 0x99, 0x20, + 0x1c, 0xa8, 0xa6, 0xb3, 0xa1, 0x95, 0xfc, 0x07, 0x9d, 0xa2, 0xf1, 0xb7, 0x33, 0xc0, 0xa5, 0x3a, + ]); + +async function getSecretKey(privateKeyBytes: Uint8Array) { + return await createPrivateKeyFromBytes(privateKeyBytes, /* extractable */ false); } describe('sendTransaction', () => { - let rpc: Rpc; + let rpc: Rpc; beforeEach(() => { rpc = createLocalhostSolanaRpc(); }); @@ -86,7 +107,7 @@ describe('sendTransaction', () => { it('returns the transaction signature', async () => { expect.assertions(1); const [secretKey, { value: latestBlockhash }] = await Promise.all([ - getSecretKey(), + getSecretKey(MOCK_PRIVATE_KEY_BYTES), rpc.getLatestBlockhash({ commitment: 'processed' }).send(), ]); const message = getMockTransactionMessage({ @@ -112,7 +133,7 @@ describe('sendTransaction', () => { }); }); it('fatals when called with a transaction having an invalid signature', async () => { - expect.assertions(3); + expect.assertions(1); const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'processed' }).send(); const message = getMockTransactionMessage({ blockhash: latestBlockhash.blockhash, @@ -132,16 +153,14 @@ describe('sendTransaction', () => { { encoding: 'base64', preflightCommitment: 'processed' }, ) .send(); - await expect(resultPromise).rejects.toThrow(RpcError); - await expect(resultPromise).rejects.toThrow(/Transaction signature verification failure/); - await expect(resultPromise).rejects.toMatchObject({ - code: -32003 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE'], - }); + await expect(resultPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE), + ); }); it('fatals when called with a transaction having an unsupported version', async () => { - expect.assertions(3); + expect.assertions(1); const [secretKey, { value: latestBlockhash }] = await Promise.all([ - getSecretKey(), + getSecretKey(MOCK_PRIVATE_KEY_BYTES), rpc.getLatestBlockhash({ commitment: 'processed' }).send(), ]); const message = getMockTransactionMessage({ @@ -163,17 +182,18 @@ describe('sendTransaction', () => { { encoding: 'base64', preflightCommitment: 'processed' }, ) .send(); - await expect(resultPromise).rejects.toThrow(RpcError); await expect(resultPromise).rejects.toThrow( - /invalid value: integer `126`, expected a valid transaction message version/, + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: + 'failed to deserialize solana_sdk::transaction::versioned::' + + 'VersionedTransaction: invalid value: integer `126`, expected a valid ' + + 'transaction message version', + }), ); - await expect(resultPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); }); it('fatals when called with a malformed transaction message', async () => { - expect.assertions(3); - const secretKey = await getSecretKey(); + expect.assertions(1); + const secretKey = await getSecretKey(MOCK_PRIVATE_KEY_BYTES); const message = new Uint8Array([4, 5, 6]); const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', secretKey, message)); const resultPromise = rpc @@ -188,14 +208,16 @@ describe('sendTransaction', () => { { encoding: 'base64', preflightCommitment: 'processed' }, ) .send(); - await expect(resultPromise).rejects.toThrow(RpcError); - await expect(resultPromise).rejects.toThrow(/failed to fill whole buffer/); - await expect(resultPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(resultPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: + 'failed to deserialize solana_sdk::transaction::versioned::' + + 'VersionedTransaction: io error: failed to fill whole buffer', + }), + ); }); - it('fatals when the fee payer has insufficient funds', async () => { - expect.assertions(3); + it("fatals when the fee payer's account does not exist", async () => { + expect.assertions(1); const [[secretKey, publicKeyBytes], { value: latestBlockhash }] = await Promise.all([ (async () => { const keyPair = (await crypto.subtle.generateKey('Ed25519', /* extractable */ false, [ @@ -224,17 +246,53 @@ describe('sendTransaction', () => { { encoding: 'base64', preflightCommitment: 'processed' }, ) .send(); - await expect(resultPromise).rejects.toThrow(RpcError); await expect(resultPromise).rejects.toThrow( - /Attempt to debit an account but found no record of a prior credit/, + new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, { + accounts: null, + cause: new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_NOT_FOUND), + logs: [], + returnData: null, + unitsConsumed: 0, + }), ); - await expect(resultPromise).rejects.toMatchObject({ - code: -32002 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE'], + }); + it('fatals when the fee payer has insufficient funds to pay rent', async () => { + expect.assertions(1); + const [secretKey, { value: latestBlockhash }] = await Promise.all([ + getSecretKey(MOCK_INSUFFICIENT_BALANCE_PRIVATE_KEY_BYTES), + rpc.getLatestBlockhash({ commitment: 'processed' }).send(), + ]); + const message = getMockTransactionMessage({ + blockhash: latestBlockhash.blockhash, + feePayerAddressBytes: MOCK_INSUFFICIENT_BALANCE_PUBLIC_KEY_BYTES, + memoString: `Hello from the web3.js tests! [${performance.now()}]`, }); + const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', secretKey, message)); + const resultPromise = rpc + .sendTransaction( + Buffer.from( + new Uint8Array([ + 0x1, // Length of signatures + ...signature, + ...message, + ]), + ).toString('base64') as Base64EncodedWireTransaction, + { encoding: 'base64', preflightCommitment: 'processed' }, + ) + .send(); + await expect(resultPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, { + accounts: null, + cause: new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE), + logs: [], + returnData: null, + unitsConsumed: 0, + }), + ); }); it('fatals when the blockhash does not exist', async () => { - expect.assertions(3); - const secretKey = await getSecretKey(); + expect.assertions(1); + const secretKey = await getSecretKey(MOCK_PRIVATE_KEY_BYTES); const message = getMockTransactionMessage({ blockhash: getBase58Decoder().decode(new Uint8Array(Array(32).fill(0))), feePayerAddressBytes: MOCK_PUBLIC_KEY_BYTES, @@ -253,17 +311,21 @@ describe('sendTransaction', () => { { encoding: 'base64', preflightCommitment: 'processed' }, ) .send(); - await expect(resultPromise).rejects.toThrow(RpcError); - await expect(resultPromise).rejects.toThrow(/Blockhash not found/); - await expect(resultPromise).rejects.toMatchObject({ - code: -32002 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE'], - }); + await expect(resultPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, { + accounts: null, + cause: new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND), + logs: [], + returnData: null, + unitsConsumed: 0, + }), + ); }); describe('when called with a `minContextSlot` higher than the highest slot available', () => { it('throws an error', async () => { - expect.assertions(2); + expect.assertions(3); const [secretKey, { value: latestBlockhash }] = await Promise.all([ - getSecretKey(), + getSecretKey(MOCK_PRIVATE_KEY_BYTES), rpc.getLatestBlockhash({ commitment: 'processed' }).send(), ]); const message = getMockTransactionMessage({ @@ -288,10 +350,14 @@ describe('sendTransaction', () => { }, ) .send(); - await expect(resultPromise).rejects.toThrow(RpcError); - await expect(resultPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(resultPromise).rejects.toThrow(SolanaError), + expect(resultPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(resultPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); }); }); diff --git a/packages/rpc-api/src/__tests__/simulate-transaction-test.ts b/packages/rpc-api/src/__tests__/simulate-transaction-test.ts index 37a88152e98d..be55220b35de 100644 --- a/packages/rpc-api/src/__tests__/simulate-transaction-test.ts +++ b/packages/rpc-api/src/__tests__/simulate-transaction-test.ts @@ -3,10 +3,15 @@ import { Buffer } from 'node:buffer'; import type { Address } from '@solana/addresses'; import { fixEncoder } from '@solana/codecs-core'; import { getBase58Decoder, getBase58Encoder } from '@solana/codecs-strings'; +import { + SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE, + SolanaError, +} from '@solana/errors'; import { createPrivateKeyFromBytes } from '@solana/keys'; import type { Rpc } from '@solana/rpc-spec'; -import { RpcError } from '@solana/rpc-spec-types'; -import type { Base58EncodedBytes, Commitment, SolanaRpcErrorCode } from '@solana/rpc-types'; +import type { Base58EncodedBytes, Commitment } from '@solana/rpc-types'; import type { Base64EncodedWireTransaction } from '@solana/transactions'; import { GetLatestBlockhashApi, SimulateTransactionApi } from '../index'; @@ -177,7 +182,7 @@ describe('simulateTransaction', () => { }); it('throws when called with a `minContextSlot` higher than the highest slot available', async () => { - expect.assertions(2); + expect.assertions(3); const [secretKey, { value: latestBlockhash }] = await Promise.all([ getSecretKey(), rpc.getLatestBlockhash({ commitment: 'processed' }).send(), @@ -204,14 +209,18 @@ describe('simulateTransaction', () => { }, ) .send(); - await expect(resultPromise).rejects.toThrow(RpcError); - await expect(resultPromise).rejects.toMatchObject({ - code: -32016 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED'], - }); + await Promise.all([ + expect(resultPromise).rejects.toThrow(SolanaError), + expect(resultPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, + ), + expect(resultPromise).rejects.toHaveProperty('context.contextSlot', expect.any(Number)), + ]); }); it('throws when called with an invalid signature if `sigVerify` is true', async () => { - expect.assertions(3); + expect.assertions(1); const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'processed' }).send(); const message = getMockTransactionMessage({ blockhash: latestBlockhash.blockhash, @@ -236,11 +245,9 @@ describe('simulateTransaction', () => { ) .send(); - await expect(resultPromise).rejects.toThrow(RpcError); - await expect(resultPromise).rejects.toThrow(/Transaction signature verification failure/); - await expect(resultPromise).rejects.toMatchObject({ - code: -32003 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE'], - }); + await expect(resultPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE), + ); }); it('does not throw when called with an invalid signature when `sigVerify` is false', async () => { @@ -358,7 +365,7 @@ describe('simulateTransaction', () => { }); it('throws when called with a transaction having an unsupported version', async () => { - expect.assertions(3); + expect.assertions(1); const [secretKey, { value: latestBlockhash }] = await Promise.all([ getSecretKey(), rpc.getLatestBlockhash({ commitment: 'processed' }).send(), @@ -382,18 +389,18 @@ describe('simulateTransaction', () => { { commitment: 'processed', encoding: 'base64' }, ) .send(); - - await expect(resultPromise).rejects.toThrow(RpcError); await expect(resultPromise).rejects.toThrow( - /invalid value: integer `126`, expected a valid transaction message version/, + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: + 'failed to deserialize solana_sdk::transaction::versioned::' + + 'VersionedTransaction: invalid value: integer `126`, expected a valid ' + + 'transaction message version', + }), ); - await expect(resultPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); }); it('throws when called with a malformed transaction message', async () => { - expect.assertions(3); + expect.assertions(1); const secretKey = await getSecretKey(); const message = new Uint8Array([4, 5, 6]); const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', secretKey, message)); @@ -409,12 +416,13 @@ describe('simulateTransaction', () => { { commitment: 'processed', encoding: 'base64' }, ) .send(); - - await expect(resultPromise).rejects.toThrow(RpcError); - await expect(resultPromise).rejects.toThrow(/failed to fill whole buffer/); - await expect(resultPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], - }); + await expect(resultPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, { + __serverMessage: + 'failed to deserialize solana_sdk::transaction::versioned::' + + 'VersionedTransaction: io error: failed to fill whole buffer', + }), + ); }); it('returns an AccountNotFound error when the fee payer is an unknown account', async () => { diff --git a/packages/rpc-spec-types/src/index.ts b/packages/rpc-spec-types/src/index.ts index e52d76c71eed..a151cd974c6f 100644 --- a/packages/rpc-spec-types/src/index.ts +++ b/packages/rpc-spec-types/src/index.ts @@ -1,5 +1,4 @@ export * from './overloads'; -export * from './rpc-error'; export * from './rpc-message'; export * from './rpc-response'; export * from './type-helpers'; diff --git a/packages/rpc-spec-types/src/rpc-error.ts b/packages/rpc-spec-types/src/rpc-error.ts deleted file mode 100644 index 4cb0ff17dd52..000000000000 --- a/packages/rpc-spec-types/src/rpc-error.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type RpcErrorResponse = Readonly<{ - code: number; - data?: unknown; - message: string; -}>; - -export class RpcError extends Error { - readonly code: number; - readonly data: unknown; - constructor(details: RpcErrorResponse) { - super(`JSON-RPC 2.0 error (${details.code}): ${details.message}`); - Error.captureStackTrace(this, this.constructor); - this.code = details.code; - this.data = details.data; - } - get name() { - return 'RpcError'; - } -} diff --git a/packages/rpc-spec-types/src/rpc-response.ts b/packages/rpc-spec-types/src/rpc-response.ts index 5ff57593449c..9b391507e5ef 100644 --- a/packages/rpc-spec-types/src/rpc-response.ts +++ b/packages/rpc-spec-types/src/rpc-response.ts @@ -1,7 +1,11 @@ -import { RpcErrorResponse } from './rpc-error'; - interface IHasIdentifier { readonly id: number; } +type RpcErrorResponse = Readonly<{ + code: number; + data?: unknown; + message: string; +}>; + export type RpcResponse = IHasIdentifier & Readonly<{ error: RpcErrorResponse } | { result: TResponse }>; diff --git a/packages/rpc-spec/package.json b/packages/rpc-spec/package.json index 4e1628cd5dee..68c087b7ebce 100644 --- a/packages/rpc-spec/package.json +++ b/packages/rpc-spec/package.json @@ -63,6 +63,7 @@ "maintained node versions" ], "dependencies": { + "@solana/errors": "workspace:*", "@solana/rpc-spec-types": "workspace:*" }, "bundlewatch": { diff --git a/packages/rpc-spec/src/__tests__/rpc-test.ts b/packages/rpc-spec/src/__tests__/rpc-test.ts index 899e0d597ddf..37f60459afcf 100644 --- a/packages/rpc-spec/src/__tests__/rpc-test.ts +++ b/packages/rpc-spec/src/__tests__/rpc-test.ts @@ -1,4 +1,5 @@ -import { createRpcMessage, RpcError } from '@solana/rpc-spec-types'; +import { SOLANA_ERROR__JSON_RPC__PARSE_ERROR, SolanaError } from '@solana/errors'; +import { createRpcMessage } from '@solana/rpc-spec-types'; import { createRpc, Rpc } from '../rpc'; import { RpcApi } from '../rpc-api'; @@ -39,12 +40,14 @@ describe('JSON-RPC 2.0', () => { expect(result).toBe(123); }); it('throws errors from the transport', async () => { - expect.assertions(3); - (makeHttpRequest as jest.Mock).mockResolvedValueOnce({ error: { code: 123, data: 'abc', message: 'o no' } }); + expect.assertions(1); + (makeHttpRequest as jest.Mock).mockResolvedValueOnce({ + error: { code: SOLANA_ERROR__JSON_RPC__PARSE_ERROR, message: 'o no' }, + }); const sendPromise = rpc.someMethod().send(); - await expect(sendPromise).rejects.toThrow(RpcError); - await expect(sendPromise).rejects.toThrow(/o no/); - await expect(sendPromise).rejects.toMatchObject({ code: 123, data: 'abc' }); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__JSON_RPC__PARSE_ERROR, { __serverMessage: 'o no' }), + ); }); describe('when calling a method having a concrete implementation', () => { let rpc: Rpc; diff --git a/packages/rpc-spec/src/rpc.ts b/packages/rpc-spec/src/rpc.ts index 950369766623..3d44e86ff0d1 100644 --- a/packages/rpc-spec/src/rpc.ts +++ b/packages/rpc-spec/src/rpc.ts @@ -1,9 +1,9 @@ +import { getSolanaErrorFromJsonRpcError } from '@solana/errors'; import { Callable, createRpcMessage, Flatten, OverloadImplementations, - RpcError, RpcResponse, UnionToIntersection, } from '@solana/rpc-spec-types'; @@ -75,7 +75,7 @@ function createPendingRpcRequest { ); }); it('fatals when the server responds with an error', async () => { - expect.assertions(3); + expect.assertions(1); iterable.mockImplementation(async function* () { yield { - error: { code: 123, data: 'abc', message: 'o no' }, + error: { code: 123, message: 'o no' }, id: 0, }; }); const subscribePromise = rpc.thingNotifications().subscribe({ abortSignal: new AbortController().signal }); - await expect(subscribePromise).rejects.toThrow(RpcError); - await expect(subscribePromise).rejects.toThrow(/o no/); - await expect(subscribePromise).rejects.toMatchObject({ code: 123, data: 'abc' }); + await expect(subscribePromise).rejects.toThrow(new SolanaError(123 as SolanaErrorCode, undefined)); }); it('throws errors when the connection fails to construct', async () => { expect.assertions(1); diff --git a/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts b/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts index 33b5ebabbd29..96948c0474a3 100644 --- a/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts +++ b/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts @@ -3,12 +3,12 @@ import { SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID, SolanaError, } from '@solana/errors'; +import { getSolanaErrorFromJsonRpcError } from '@solana/errors'; import { Callable, createRpcMessage, Flatten, OverloadImplementations, - RpcError, RpcResponse, UnionToIntersection, } from '@solana/rpc-spec-types'; @@ -159,7 +159,7 @@ function createPendingRpcSubscription< >) { if ('id' in message && message.id === subscribeMessage.id) { if ('error' in message) { - throw new RpcError(message.error); + throw getSolanaErrorFromJsonRpcError(message.error); } else { subscriptionId = message.result as RpcSubscriptionId; break; diff --git a/packages/rpc-types/src/index.ts b/packages/rpc-types/src/index.ts index e34e5b5a5386..a915f7a8bb77 100644 --- a/packages/rpc-types/src/index.ts +++ b/packages/rpc-types/src/index.ts @@ -6,7 +6,6 @@ export * from './commitment'; export * from './encoded-bytes'; export * from './lamports'; export * from './rpc-api'; -export * from './rpc-errors'; export * from './stringified-bigint'; export * from './stringified-number'; export * from './token-amount'; diff --git a/packages/rpc-types/src/rpc-errors.ts b/packages/rpc-types/src/rpc-errors.ts deleted file mode 100644 index 86f1f96d6f89..000000000000 --- a/packages/rpc-types/src/rpc-errors.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Keep in sync with https://github.com/solana-labs/solana/blob/master/rpc-client-api/src/custom_error.rs -// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/ -export const SolanaRpcErrorCode = { - JSON_RPC_INVALID_PARAMS: -32602, - JSON_RPC_SCAN_ERROR: -32012, - JSON_RPC_SERVER_ERROR_BLOCK_CLEANED_UP: -32001, - JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE: -32004, - JSON_RPC_SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET: -32014, - JSON_RPC_SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX: -32010, - JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED: -32009, - JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED: -32016, - JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY: -32005, - JSON_RPC_SERVER_ERROR_NO_SNAPSHOT: -32008, - JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE: -32002, - JSON_RPC_SERVER_ERROR_SLOT_SKIPPED: -32007, - JSON_RPC_SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE: -32011, - JSON_RPC_SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE: -32006, - JSON_RPC_SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH: -32013, - JSON_RPC_SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE: -32003, - JSON_RPC_SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION: -32015, -} as const; - -export type SolanaRpcErrorCodeEnum = (typeof SolanaRpcErrorCode)[keyof typeof SolanaRpcErrorCode]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c99181b85209..f173c75f143e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -632,6 +632,9 @@ importers: '@solana/codecs-strings': specifier: workspace:* version: link:../codecs-strings + '@solana/errors': + specifier: workspace:* + version: link:../errors '@solana/keys': specifier: workspace:* version: link:../keys @@ -703,6 +706,9 @@ importers: packages/rpc-spec: dependencies: + '@solana/errors': + specifier: workspace:* + version: link:../errors '@solana/rpc-spec-types': specifier: workspace:* version: link:../rpc-spec-types diff --git a/scripts/fixtures/send-transaction-fee-payer-insufficient-funds.json b/scripts/fixtures/send-transaction-fee-payer-insufficient-funds.json new file mode 100644 index 000000000000..6e0dc72f958e --- /dev/null +++ b/scripts/fixtures/send-transaction-fee-payer-insufficient-funds.json @@ -0,0 +1,10 @@ +{ + "pubkey": "6Zs91PMyhqyMgNVuT8EnM6f3YjVAVabvVWLjpFSC288q", + "account": { + "lamports": 1, + "data": ["", "base64"], + "owner": "11111111111111111111111111111111", + "executable": false, + "rentEpoch": 18446744073709551615 + } +}