Skip to content

Commit

Permalink
Create coded exceptions for invariant violations
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Mar 4, 2024
1 parent 27479b3 commit 1041fb6
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 17 deletions.
10 changes: 10 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR = 55 as const;
export const SOLANA_ERROR__EXPECTED_INSTRUCTION_TO_HAVE_ACCOUNTS = 70 as const;
export const SOLANA_ERROR__EXPECTED_INSTRUCTION_TO_HAVE_DATA = 71 as const;
export const SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH = 72 as const;
// Reserve error codes starting with [3507000-3507999] for invariant violations
export const SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING = 3507000 as const;
export const SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE =
3507001 as const;
export const SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING = 3507002 as const;
export const SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE = 3507003 as const;
// Reserve error codes starting with [4615000-4615999] for the Rust enum `InstructionError`
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN = 4615000 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_GENERIC_ERROR = 4615001 as const;
Expand Down Expand Up @@ -322,6 +328,10 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_ACCOUNTS_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_INSTRUCTION_TRACE_LENGTH_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_BUILTIN_PROGRAMS_MUST_CONSUME_COMPUTE_UNITS
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_CANNOT_CREATE_SUBSCRIPTION_REQUEST
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_EXPECTED_SERVER_SUBSCRIPTION_ID
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED
Expand Down
8 changes: 8 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ import {
SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR,
SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH,
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED,
Expand Down Expand Up @@ -275,6 +277,12 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: {
byteLength: number;
};
[SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING]: {
cacheKey: string;
};
[SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE]: {
unexpectedValue: unknown;
};
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: {
value: string;
};
Expand Down
19 changes: 19 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ import {
SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH,
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
SOLANA_ERROR__INVALID_SEEDS_POINT_ON_CURVE,
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
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__LAMPORTS_OUT_OF_RANGE,
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
Expand Down Expand Up @@ -302,6 +306,21 @@ export const SolanaErrorMessages: Readonly<{
'Expected instruction to have progress address $expectedProgramAddress, got $actualProgramAddress.',
[SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: 'Key pair bytes must be of length 64, got $byteLength.',
[SOLANA_ERROR__INVALID_SEEDS_POINT_ON_CURVE]: 'Invalid seeds; point must fall off the Ed25519 curve.',
[SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING]:
'Invariant violation: Found no abortable iterable cache entry for key `$cacheKey`. It ' +
'should be impossible to hit this error; please file an issue at ' +
'https://sola.na/web3invariant',
[SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE]:
'Invariant violation: Switch statement non-exhaustive. Received unexpected value ' +
'`$unexpectedValue`. It should be impossible to hit this error; please file an issue at ' +
'https://sola.na/web3invariant',
[SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE]:
'Invariant violation: WebSocket message iterator state is corrupt; iterated without first ' +
'resolving existing message promise. It should be impossible to hit this error; please ' +
'file an issue at https://sola.na/web3invariant',
[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__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]',
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`',
[SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
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__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED,
SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CONNECTION_CLOSED,
SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_FAILED_TO_CONNECT,
Expand Down Expand Up @@ -132,13 +134,14 @@ export async function createWebSocketConnection({
const state = iteratorState.get(iteratorKey);
if (!state) {
// There should always be state by now.
throw new Error('Invariant: WebSocket message iterator is missing state storage');
throw new SolanaError(
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
);
}
if (state.__hasPolled) {
// You should never be able to poll twice in a row.
throw new Error(
'Invariant: WebSocket message iterator state is corrupt; ' +
'iterated without first resolving existing message promise',
throw new SolanaError(
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
);
}
const queuedMessages = state.queuedMessages;
Expand Down
11 changes: 8 additions & 3 deletions packages/rpc-subscriptions/src/cached-abortable-iterable.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SolanaError,
} from '@solana/errors';

type CacheEntry<TIterable extends AsyncIterable<unknown>> = {
abortController: AbortController;
iterable: Promise<TIterable> | TIterable;
Expand All @@ -7,7 +12,6 @@ type CacheEntry<TIterable extends AsyncIterable<unknown>> = {
type CacheKey = string | symbol;
type Config<TInput extends unknown[], TIterable extends AsyncIterable<unknown>> = Readonly<{
getAbortSignalFromInputArgs: (...args: TInput) => AbortSignal;
getCacheEntryMissingErrorMessage?: (cacheKey: CacheKey) => string;
getCacheKeyFromInputArgs: (...args: TInput) =>
| CacheKey
// `undefined` implies 'do not cache'
Expand All @@ -32,7 +36,6 @@ function registerIterableCleanup(iterable: AsyncIterable<unknown>, cleanupFn: Ca

export function getCachedAbortableIterableFactory<TInput extends unknown[], TIterable extends AsyncIterable<unknown>>({
getAbortSignalFromInputArgs,
getCacheEntryMissingErrorMessage,
getCacheKeyFromInputArgs,
onCacheHit,
onCreateIterable,
Expand All @@ -41,7 +44,9 @@ export function getCachedAbortableIterableFactory<TInput extends unknown[], TIte
function getCacheEntryOrThrow(cacheKey: CacheKey) {
const currentCacheEntry = cache.get(cacheKey);
if (!currentCacheEntry) {
throw new Error(getCacheEntryMissingErrorMessage ? getCacheEntryMissingErrorMessage(cacheKey) : undefined);
throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING, {
cacheKey: cacheKey.toString(),
});
}
return currentCacheEntry;
}
Expand Down
4 changes: 0 additions & 4 deletions packages/rpc-subscriptions/src/rpc-subscriptions-coalescer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ export function getRpcSubscriptionsWithSubscriptionCoalescing<TRpcSubscriptionsM
AsyncIterable<unknown>
>({
getAbortSignalFromInputArgs: ({ abortSignal }) => abortSignal,
getCacheEntryMissingErrorMessage: __DEV__
? deduplicationKey =>
`Invariant: Found no cache entry for subscription with deduplication key \`${deduplicationKey?.toString()}\``
: undefined,
getCacheKeyFromInputArgs: () => deduplicationKey,
async onCacheHit(_iterable, _config) {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ export function getWebSocketTransportWithConnectionSharding<TTransport extends R
}: Config<TTransport>): TTransport {
return getCachedAbortableIterableFactory({
getAbortSignalFromInputArgs: ({ signal }) => signal,
getCacheEntryMissingErrorMessage: __DEV__
? shardKey => `Invariant: Found no cache entry for connection with shard key \`${shardKey?.toString()}\``
: undefined,
getCacheKeyFromInputArgs: ({ payload }) => (getShard ? getShard(payload) : NULL_SHARD_CACHE_KEY),
onCacheHit: (connection, { payload }) => connection.send_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(payload),
onCreateIterable: (abortSignal, config) =>
Expand Down
8 changes: 5 additions & 3 deletions packages/rpc-types/src/commitment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE, SolanaError } from '@solana/errors';

export type Commitment = 'confirmed' | 'finalized' | 'processed';

function getCommitmentScore(commitment: Commitment): number {
Expand All @@ -9,9 +11,9 @@ function getCommitmentScore(commitment: Commitment): number {
case 'processed':
return 0;
default:
return ((_: never) => {
throw new Error(`Unrecognized commitment \`${commitment}\`.`);
})(commitment);
throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE, {
unexpectedValue: commitment satisfies never,
});
}
}

Expand Down

0 comments on commit 1041fb6

Please sign in to comment.