Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Commit 03cfae6

Browse files
steveluscherpull[bot]
authored andcommitted
Create coded exceptions for invariant violations (#2242)
# Summary Invariant violations are a kind of error that should never get hit ever, and if they do your program is broken. In one sense we don't want error codes for them because they're not intended to be caught in downstream programs. On the other hand I've tried to come up with a reason _not_ to give them codes and I can't. Making them a `@solana/error` gives us compression and error decoding for free. Addresses #2118.
1 parent e8f7178 commit 03cfae6

File tree

8 files changed

+57
-17
lines changed

8 files changed

+57
-17
lines changed

packages/errors/src/codes.ts

+10
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ export const SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR = 55 as const;
4545
export const SOLANA_ERROR__EXPECTED_INSTRUCTION_TO_HAVE_ACCOUNTS = 70 as const;
4646
export const SOLANA_ERROR__EXPECTED_INSTRUCTION_TO_HAVE_DATA = 71 as const;
4747
export const SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH = 72 as const;
48+
// Reserve error codes starting with [3507000-3507999] for invariant violations
49+
export const SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING = 3507000 as const;
50+
export const SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE =
51+
3507001 as const;
52+
export const SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING = 3507002 as const;
53+
export const SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE = 3507003 as const;
4854
// Reserve error codes starting with [4615000-4615999] for the Rust enum `InstructionError`
4955
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN = 4615000 as const;
5056
export const SOLANA_ERROR__INSTRUCTION_ERROR_GENERIC_ERROR = 4615001 as const;
@@ -322,6 +328,10 @@ export type SolanaErrorCode =
322328
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_ACCOUNTS_EXCEEDED
323329
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_INSTRUCTION_TRACE_LENGTH_EXCEEDED
324330
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_BUILTIN_PROGRAMS_MUST_CONSUME_COMPUTE_UNITS
331+
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING
332+
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
333+
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING
334+
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE
325335
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_CANNOT_CREATE_SUBSCRIPTION_REQUEST
326336
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_EXPECTED_SERVER_SUBSCRIPTION_ID
327337
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED

packages/errors/src/context.ts

+8
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ import {
7676
SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR,
7777
SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH,
7878
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
79+
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
80+
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
7981
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
8082
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
8183
SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED,
@@ -275,6 +277,12 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
275277
[SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: {
276278
byteLength: number;
277279
};
280+
[SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING]: {
281+
cacheKey: string;
282+
};
283+
[SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE]: {
284+
unexpectedValue: unknown;
285+
};
278286
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: {
279287
value: string;
280288
};

packages/errors/src/messages.ts

+19
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ import {
8181
SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH,
8282
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
8383
SOLANA_ERROR__INVALID_SEEDS_POINT_ON_CURVE,
84+
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
85+
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
86+
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
87+
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
8488
SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE,
8589
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
8690
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
@@ -302,6 +306,21 @@ export const SolanaErrorMessages: Readonly<{
302306
'Expected instruction to have progress address $expectedProgramAddress, got $actualProgramAddress.',
303307
[SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: 'Key pair bytes must be of length 64, got $byteLength.',
304308
[SOLANA_ERROR__INVALID_SEEDS_POINT_ON_CURVE]: 'Invalid seeds; point must fall off the Ed25519 curve.',
309+
[SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING]:
310+
'Invariant violation: Found no abortable iterable cache entry for key `$cacheKey`. It ' +
311+
'should be impossible to hit this error; please file an issue at ' +
312+
'https://sola.na/web3invariant',
313+
[SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE]:
314+
'Invariant violation: Switch statement non-exhaustive. Received unexpected value ' +
315+
'`$unexpectedValue`. It should be impossible to hit this error; please file an issue at ' +
316+
'https://sola.na/web3invariant',
317+
[SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE]:
318+
'Invariant violation: WebSocket message iterator state is corrupt; iterated without first ' +
319+
'resolving existing message promise. It should be impossible to hit this error; please ' +
320+
'file an issue at https://sola.na/web3invariant',
321+
[SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING]:
322+
'Invariant violation: WebSocket message iterator is missing state storage. It should be ' +
323+
'impossible to hit this error; please file an issue at https://sola.na/web3invariant',
305324
[SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]',
306325
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`',
307326
[SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`',

packages/rpc-subscriptions-transport-websocket/src/websocket-connection.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {
2+
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
3+
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
24
SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED,
35
SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CONNECTION_CLOSED,
46
SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_FAILED_TO_CONNECT,
@@ -132,13 +134,14 @@ export async function createWebSocketConnection({
132134
const state = iteratorState.get(iteratorKey);
133135
if (!state) {
134136
// There should always be state by now.
135-
throw new Error('Invariant: WebSocket message iterator is missing state storage');
137+
throw new SolanaError(
138+
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
139+
);
136140
}
137141
if (state.__hasPolled) {
138142
// You should never be able to poll twice in a row.
139-
throw new Error(
140-
'Invariant: WebSocket message iterator state is corrupt; ' +
141-
'iterated without first resolving existing message promise',
143+
throw new SolanaError(
144+
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
142145
);
143146
}
144147
const queuedMessages = state.queuedMessages;

packages/rpc-subscriptions/src/cached-abortable-iterable.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
3+
SolanaError,
4+
} from '@solana/errors';
5+
16
type CacheEntry<TIterable extends AsyncIterable<unknown>> = {
27
abortController: AbortController;
38
iterable: Promise<TIterable> | TIterable;
@@ -7,7 +12,6 @@ type CacheEntry<TIterable extends AsyncIterable<unknown>> = {
712
type CacheKey = string | symbol;
813
type Config<TInput extends unknown[], TIterable extends AsyncIterable<unknown>> = Readonly<{
914
getAbortSignalFromInputArgs: (...args: TInput) => AbortSignal;
10-
getCacheEntryMissingErrorMessage?: (cacheKey: CacheKey) => string;
1115
getCacheKeyFromInputArgs: (...args: TInput) =>
1216
| CacheKey
1317
// `undefined` implies 'do not cache'
@@ -32,7 +36,6 @@ function registerIterableCleanup(iterable: AsyncIterable<unknown>, cleanupFn: Ca
3236

3337
export function getCachedAbortableIterableFactory<TInput extends unknown[], TIterable extends AsyncIterable<unknown>>({
3438
getAbortSignalFromInputArgs,
35-
getCacheEntryMissingErrorMessage,
3639
getCacheKeyFromInputArgs,
3740
onCacheHit,
3841
onCreateIterable,
@@ -41,7 +44,9 @@ export function getCachedAbortableIterableFactory<TInput extends unknown[], TIte
4144
function getCacheEntryOrThrow(cacheKey: CacheKey) {
4245
const currentCacheEntry = cache.get(cacheKey);
4346
if (!currentCacheEntry) {
44-
throw new Error(getCacheEntryMissingErrorMessage ? getCacheEntryMissingErrorMessage(cacheKey) : undefined);
47+
throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING, {
48+
cacheKey: cacheKey.toString(),
49+
});
4550
}
4651
return currentCacheEntry;
4752
}

packages/rpc-subscriptions/src/rpc-subscriptions-coalescer.ts

-4
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@ export function getRpcSubscriptionsWithSubscriptionCoalescing<TRpcSubscriptionsM
6060
AsyncIterable<unknown>
6161
>({
6262
getAbortSignalFromInputArgs: ({ abortSignal }) => abortSignal,
63-
getCacheEntryMissingErrorMessage: __DEV__
64-
? deduplicationKey =>
65-
`Invariant: Found no cache entry for subscription with deduplication key \`${deduplicationKey?.toString()}\``
66-
: undefined,
6763
getCacheKeyFromInputArgs: () => deduplicationKey,
6864
async onCacheHit(_iterable, _config) {
6965
/**

packages/rpc-subscriptions/src/rpc-subscriptions-connection-sharding.ts

-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ export function getWebSocketTransportWithConnectionSharding<TTransport extends R
2222
}: Config<TTransport>): TTransport {
2323
return getCachedAbortableIterableFactory({
2424
getAbortSignalFromInputArgs: ({ signal }) => signal,
25-
getCacheEntryMissingErrorMessage: __DEV__
26-
? shardKey => `Invariant: Found no cache entry for connection with shard key \`${shardKey?.toString()}\``
27-
: undefined,
2825
getCacheKeyFromInputArgs: ({ payload }) => (getShard ? getShard(payload) : NULL_SHARD_CACHE_KEY),
2926
onCacheHit: (connection, { payload }) => connection.send_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(payload),
3027
onCreateIterable: (abortSignal, config) =>

packages/rpc-types/src/commitment.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE, SolanaError } from '@solana/errors';
2+
13
export type Commitment = 'confirmed' | 'finalized' | 'processed';
24

35
function getCommitmentScore(commitment: Commitment): number {
@@ -9,9 +11,9 @@ function getCommitmentScore(commitment: Commitment): number {
911
case 'processed':
1012
return 0;
1113
default:
12-
return ((_: never) => {
13-
throw new Error(`Unrecognized commitment \`${commitment}\`.`);
14-
})(commitment);
14+
throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE, {
15+
unexpectedValue: commitment satisfies never,
16+
});
1517
}
1618
}
1719

0 commit comments

Comments
 (0)