From cf9c20ceed7186f5af704ee646344c42d4ec0084 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Sun, 3 Mar 2024 19:04:30 -0800 Subject: [PATCH] Convert all errors in `@solana/rpc-subscriptions-*` to coded exceptions (#2236) --- packages/errors/src/codes.ts | 11 ++++++++ packages/errors/src/context.ts | 8 ++++++ packages/errors/src/messages.ts | 14 +++++++++++ packages/rpc-subscriptions-spec/package.json | 1 + .../src/__tests__/rpc-subscription-test.ts | 19 +++++++++++--- .../src/rpc-subscriptions.ts | 25 ++++++++++--------- .../package.json | 1 + .../__tests__/websocket-connection-test.ts | 25 ++++++++++++++++--- .../src/websocket-connection.ts | 21 +++++++++++----- .../src/rpc-subscriptions-coalescer.ts | 3 +-- .../rpc-subscriptions-connection-sharding.ts | 5 ++-- pnpm-lock.yaml | 6 +++++ 12 files changed, 110 insertions(+), 29 deletions(-) diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index efef7d260b4..a4e47599632 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -184,6 +184,12 @@ export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_LOADED_ACCOUNTS_DATA_SIZE_L export const SOLANA_ERROR__TRANSACTION_ERROR_RESANITIZATION_NEEDED = 7050034 as const; export const SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED = 7050035 as const; export const SOLANA_ERROR__TRANSACTION_ERROR_UNBALANCED_TRANSACTION = 7050036 as const; +// Reserve subscription-related error codes in the range [8160000-8160999] +export const SOLANA_ERROR__RPC_SUBSCRIPTIONS_CANNOT_CREATE_SUBSCRIPTION_REQUEST = 8190000 as const; +export const SOLANA_ERROR__RPC_SUBSCRIPTIONS_EXPECTED_SERVER_SUBSCRIPTION_ID = 8190001 as const; +export const SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED = 8190002 as const; +export const SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CONNECTION_CLOSED = 8190003 as const; +export const SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_FAILED_TO_CONNECT = 8190004 as const; /** * A union of every Solana error code @@ -322,6 +328,11 @@ 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__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 + | typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CONNECTION_CLOSED + | typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_FAILED_TO_CONNECT | typeof SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN | typeof SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_IN_USE | typeof SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_LOADED_TWICE diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 5475f098e46..69a3f22bb15 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -89,6 +89,7 @@ import { SOLANA_ERROR__RPC_INTEGER_OVERFLOW, SOLANA_ERROR__RPC_TRANSPORT_HEADER_FORBIDDEN, SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR, + SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT, SOLANA_ERROR__SIGNER_ADDRESS_CANNOT_HAVE_MULTIPLE_SIGNERS, SOLANA_ERROR__SIGNER_EXPECTED_KEY_PAIR_SIGNER, SOLANA_ERROR__SIGNER_EXPECTED_MESSAGE_MODIFYING_SIGNER, @@ -98,6 +99,7 @@ import { SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_PARTIAL_SIGNER, SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_SENDING_SIGNER, SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_SIGNER, + SOLANA_ERROR__SUBSCRIPTION_CANNOT_CREATE_SUBSCRIPTION_REQUEST, SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION, SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT, @@ -325,6 +327,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< message: string; statusCode: number; }; + [SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT]: { + errorEvent: Event; + }; [SOLANA_ERROR__SIGNER_ADDRESS_CANNOT_HAVE_MULTIPLE_SIGNERS]: { address: string; }; @@ -352,6 +357,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< [SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_SIGNER]: { address: string; }; + [SOLANA_ERROR__SUBSCRIPTION_CANNOT_CREATE_SUBSCRIPTION_REQUEST]: { + notificationName: string; + }; [SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE]: { value: number; }; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index c8c86bb5f4f..8ddb783e1dc 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -101,6 +101,9 @@ import { SOLANA_ERROR__RPC_INTEGER_OVERFLOW, SOLANA_ERROR__RPC_TRANSPORT_HEADER_FORBIDDEN, SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR, + SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED, + SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CONNECTION_CLOSED, + SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT, SOLANA_ERROR__SIGNER_ADDRESS_CANNOT_HAVE_MULTIPLE_SIGNERS, SOLANA_ERROR__SIGNER_EXPECTED_KEY_PAIR_SIGNER, SOLANA_ERROR__SIGNER_EXPECTED_MESSAGE_MODIFYING_SIGNER, @@ -112,6 +115,8 @@ import { SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_SIGNER, SOLANA_ERROR__SIGNER_TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS, SOLANA_ERROR__SIGNER_TRANSACTION_SENDING_SIGNER_MISSING, + SOLANA_ERROR__SUBSCRIPTION_CANNOT_CREATE_SUBSCRIPTION_REQUEST, + SOLANA_ERROR__SUBSCRIPTION_EXPECTED_SERVER_SUBSCRIPTION_ID, SOLANA_ERROR__SUBTLE_CRYPTO_DIGEST_MISSING, SOLANA_ERROR__SUBTLE_CRYPTO_ED25519_ALGORITHM_MISSING, SOLANA_ERROR__SUBTLE_CRYPTO_EXPORT_FUNCTION_MISSING, @@ -332,6 +337,10 @@ export const SolanaErrorMessages: Readonly<{ 'HTTP header(s) forbidden: $headers. Learn more at ' + 'https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name.', [SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR]: 'HTTP error ($statusCode): $message', + [SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED]: + 'WebSocket was closed before payload could be added to the send buffer', + [SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CONNECTION_CLOSED]: 'WebSocket connection closed', + [SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT]: 'WebSocket failed to connect', [SOLANA_ERROR__SIGNER_ADDRESS_CANNOT_HAVE_MULTIPLE_SIGNERS]: 'Multiple distinct signers were identified for address `$address`. Please ensure that ' + 'you are using the same signer instance for each address.', @@ -356,6 +365,11 @@ export const SolanaErrorMessages: Readonly<{ [SOLANA_ERROR__SIGNER_TRANSACTION_SENDING_SIGNER_MISSING]: 'No `TransactionSendingSigner` was identified. Please provide a valid ' + '`ITransactionWithSingleSendingSigner` transaction.', + [SOLANA_ERROR__SUBSCRIPTION_CANNOT_CREATE_SUBSCRIPTION_REQUEST]: + "Either the notification name must end in 'Notifications' or the API must supply a " + + "subscription creator function for the notification '$notificationName' to map between " + + 'the notification name and the subscribe/unsubscribe method names.', + [SOLANA_ERROR__SUBSCRIPTION_EXPECTED_SERVER_SUBSCRIPTION_ID]: 'Failed to obtain a subscription id from the server', [SOLANA_ERROR__SUBTLE_CRYPTO_DIGEST_MISSING]: 'No digest implementation could be found.', [SOLANA_ERROR__SUBTLE_CRYPTO_ED25519_ALGORITHM_MISSING]: 'This runtime does not support the generation of Ed25519 key pairs.\n\nInstall and ' + diff --git a/packages/rpc-subscriptions-spec/package.json b/packages/rpc-subscriptions-spec/package.json index 88240d3de3c..b4f114af47c 100644 --- a/packages/rpc-subscriptions-spec/package.json +++ b/packages/rpc-subscriptions-spec/package.json @@ -63,6 +63,7 @@ "maintained node versions" ], "dependencies": { + "@solana/errors": "workspace:*", "@solana/rpc-spec-types": "workspace:*" }, "devDependencies": { diff --git a/packages/rpc-subscriptions-spec/src/__tests__/rpc-subscription-test.ts b/packages/rpc-subscriptions-spec/src/__tests__/rpc-subscription-test.ts index 482bcaa000a..8d6c16ca832 100644 --- a/packages/rpc-subscriptions-spec/src/__tests__/rpc-subscription-test.ts +++ b/packages/rpc-subscriptions-spec/src/__tests__/rpc-subscription-test.ts @@ -1,3 +1,8 @@ +import { + SOLANA_ERROR__SUBSCRIPTION_CANNOT_CREATE_SUBSCRIPTION_REQUEST, + SOLANA_ERROR__SUBSCRIPTION_EXPECTED_SERVER_SUBSCRIPTION_ID, + SolanaError, +} from '@solana/errors'; import { createRpcMessage, RpcError } from '@solana/rpc-spec-types'; import { createSubscriptionRpc, RpcSubscriptions } from '../rpc-subscriptions'; @@ -223,13 +228,19 @@ describe('JSON-RPC 2.0 Subscriptions', () => { const thingNotificationsPromise = rpc .thingNotifications() .subscribe({ abortSignal: new AbortController().signal }); - await expect(thingNotificationsPromise).rejects.toThrow('Failed to obtain a subscription id'); + await expect(thingNotificationsPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__SUBSCRIPTION_EXPECTED_SERVER_SUBSCRIPTION_ID), + ); }, ); it("fatals when called with a method that does not end in 'Notifications'", () => { expect(() => { rpc.nonConformingNotif().subscribe({ abortSignal: new AbortController().signal }); - }).toThrow(); + }).toThrow( + new SolanaError(SOLANA_ERROR__SUBSCRIPTION_CANNOT_CREATE_SUBSCRIPTION_REQUEST, { + notificationName: 'nonConformingNotif', + }), + ); }); it('fatals when called with an already aborted signal', async () => { expect.assertions(1); @@ -244,7 +255,9 @@ describe('JSON-RPC 2.0 Subscriptions', () => { yield { id: 0, result: undefined /* subscription id */ }; }); const subscribePromise = rpc.thingNotifications().subscribe({ abortSignal: new AbortController().signal }); - await expect(subscribePromise).rejects.toThrow(/Failed to obtain a subscription id from the server/); + await expect(subscribePromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__SUBSCRIPTION_EXPECTED_SERVER_SUBSCRIPTION_ID), + ); }); it('fatals when the server responds with an error', async () => { expect.assertions(3); diff --git a/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts b/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts index fc9005e3b3b..be853c04c6d 100644 --- a/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts +++ b/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts @@ -1,3 +1,8 @@ +import { + SOLANA_ERROR__SUBSCRIPTION_CANNOT_CREATE_SUBSCRIPTION_REQUEST, + SOLANA_ERROR__SUBSCRIPTION_EXPECTED_SERVER_SUBSCRIPTION_ID, + SolanaError, +} from '@solana/errors'; import { Callable, createRpcMessage, @@ -76,22 +81,19 @@ function makeProxy { signal: new AbortController().signal, url: 'ws://fake', // Wrong URL! }); - await expect(connectionPromise).rejects.toThrow('WebSocket failed to connect'); + await expect(connectionPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT, { + errorEvent: {} as Event, + }), + ); }); it('throws when the connection is aborted before the connection is established', async () => { expect.assertions(2); @@ -51,7 +60,11 @@ describe('createWebSocketConnection', () => { const client = getLatestClient(); expect(client).toHaveProperty('readyState', WebSocket.CONNECTING); abortController.abort(); - await expect(connectionPromise).rejects.toThrow(); + await expect(connectionPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT, { + errorEvent: {} as Event, + }), + ); }); }); @@ -217,7 +230,9 @@ describe('RpcWebSocketConnection', () => { expect.assertions(1); const sendPromise = connection.send({ some: 'message' }); abortController.abort(); - await expect(sendPromise).rejects.toThrow(); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED), + ); }); it('fatals when the connection encounters an error while a message is queued', async () => { expect.assertions(1); @@ -227,7 +242,9 @@ describe('RpcWebSocketConnection', () => { reason: 'o no', wasClean: false, }); - await expect(sendPromise).rejects.toThrow(); + await expect(sendPromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED), + ); }); }); }); diff --git a/packages/rpc-subscriptions-transport-websocket/src/websocket-connection.ts b/packages/rpc-subscriptions-transport-websocket/src/websocket-connection.ts index 727a9f08040..86c93a9ce0c 100644 --- a/packages/rpc-subscriptions-transport-websocket/src/websocket-connection.ts +++ b/packages/rpc-subscriptions-transport-websocket/src/websocket-connection.ts @@ -1,3 +1,9 @@ +import { + SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED, + SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CONNECTION_CLOSED, + SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT, + SolanaError, +} from '@solana/errors'; import WebSocket from '@solana/ws-impl'; type Config = Readonly<{ @@ -66,8 +72,9 @@ export async function createWebSocketConnection({ function handleError(ev: Event) { if (!hasConnected) { reject( - // TODO: Coded error - new Error('WebSocket failed to connect', { cause: ev }), + new SolanaError(SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT, { + errorEvent: ev, + }), ); } } @@ -99,8 +106,9 @@ export async function createWebSocketConnection({ bufferDrainWatcher = undefined; clearInterval(intervalId); reject( - // TODO: Coded error - new Error('WebSocket was closed before payload could be sent'), + new SolanaError( + SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED, + ), ); }; }); @@ -150,8 +158,9 @@ export async function createWebSocketConnection({ if (e === EXPLICIT_ABORT_TOKEN) { return; } else { - // TODO: Coded error. - throw new Error('WebSocket connection closed', { cause: e }); + throw new SolanaError(SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CONNECTION_CLOSED, { + cause: e, + }); } } } diff --git a/packages/rpc-subscriptions/src/rpc-subscriptions-coalescer.ts b/packages/rpc-subscriptions/src/rpc-subscriptions-coalescer.ts index 60c6a791581..a0205809eb6 100644 --- a/packages/rpc-subscriptions/src/rpc-subscriptions-coalescer.ts +++ b/packages/rpc-subscriptions/src/rpc-subscriptions-coalescer.ts @@ -61,9 +61,8 @@ export function getRpcSubscriptionsWithSubscriptionCoalescing({ getAbortSignalFromInputArgs: ({ abortSignal }) => abortSignal, getCacheEntryMissingError(deduplicationKey) { - // TODO: Coded error. return new Error( - `Found no cache entry for subscription with deduplication key \`${deduplicationKey?.toString()}\``, + `Invariant: Found no cache entry for subscription with deduplication key \`${deduplicationKey?.toString()}\``, ); }, getCacheKeyFromInputArgs: () => deduplicationKey, diff --git a/packages/rpc-subscriptions/src/rpc-subscriptions-connection-sharding.ts b/packages/rpc-subscriptions/src/rpc-subscriptions-connection-sharding.ts index e6b131dccc6..7788deba6fd 100644 --- a/packages/rpc-subscriptions/src/rpc-subscriptions-connection-sharding.ts +++ b/packages/rpc-subscriptions/src/rpc-subscriptions-connection-sharding.ts @@ -23,8 +23,9 @@ export function getWebSocketTransportWithConnectionSharding signal, getCacheEntryMissingError(shardKey) { - // TODO: Coded error. - return new Error(`Found no cache entry for connection with shard key \`${shardKey?.toString()}\``); + return new Error( + `Invariant: Found no cache entry for connection with shard key \`${shardKey?.toString()}\``, + ); }, getCacheKeyFromInputArgs: ({ payload }) => (getShard ? getShard(payload) : NULL_SHARD_CACHE_KEY), onCacheHit: (connection, { payload }) => connection.send_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(payload), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1ec1cd3acf..244decb5de5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2328,6 +2328,9 @@ importers: packages/rpc-subscriptions-spec: dependencies: + '@solana/errors': + specifier: workspace:* + version: link:../errors '@solana/rpc-spec-types': specifier: workspace:* version: link:../rpc-spec-types @@ -2398,6 +2401,9 @@ importers: packages/rpc-subscriptions-transport-websocket: dependencies: + '@solana/errors': + specifier: workspace:* + version: link:../errors '@solana/rpc-subscriptions-spec': specifier: workspace:* version: link:../rpc-subscriptions-spec