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

Convert all errors in @solana/rpc-subscriptions-* to coded exceptions #2236

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
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 @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down
14 changes: 14 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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.',
Expand All @@ -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 ' +
Expand Down
1 change: 1 addition & 0 deletions packages/rpc-subscriptions-spec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"maintained node versions"
],
"dependencies": {
"@solana/errors": "workspace:*",
"@solana/rpc-spec-types": "workspace:*"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
25 changes: 13 additions & 12 deletions packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -76,22 +81,19 @@ function makeProxy<TRpcSubscriptionsApiMethods, TRpcSubscriptionsTransport exten
},
get(target, p, receiver) {
return function (...rawParams: unknown[]) {
const methodName = p.toString();
const createRpcSubscription = Reflect.get(target, methodName, receiver);
const notificationName = p.toString();
const createRpcSubscription = Reflect.get(target, notificationName, receiver);
if (p.toString().endsWith('Notifications') === false && !createRpcSubscription) {
// TODO: Coded error.
throw new Error(
"Either the notification name must end in 'Notifications' or the API " +
'must supply a subscription creator function to map between the ' +
'notification name and the subscribe/unsubscribe method names.',
);
throw new SolanaError(SOLANA_ERROR__SUBSCRIPTION_CANNOT_CREATE_SUBSCRIPTION_REQUEST, {
notificationName,
});
}
const newRequest = createRpcSubscription
? createRpcSubscription(...rawParams)
: {
params: rawParams,
subscribeMethodName: methodName.replace(/Notifications$/, 'Subscribe'),
unsubscribeMethodName: methodName.replace(/Notifications$/, 'Unsubscribe'),
subscribeMethodName: notificationName.replace(/Notifications$/, 'Subscribe'),
unsubscribeMethodName: notificationName.replace(/Notifications$/, 'Unsubscribe'),
};
return createPendingRpcSubscription(rpcConfig, newRequest);
};
Expand Down Expand Up @@ -165,8 +167,7 @@ function createPendingRpcSubscription<
}
}
if (subscriptionId == null) {
// TODO: Coded error.
throw new Error('Failed to obtain a subscription id from the server');
throw new SolanaError(SOLANA_ERROR__SUBSCRIPTION_EXPECTED_SERVER_SUBSCRIPTION_ID);
}
/**
* STEP 3: Return an iterable that yields notifications for this subscription id.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"maintained node versions"
],
"dependencies": {
"@solana/errors": "workspace:*",
"@solana/rpc-subscriptions-spec": "workspace:*"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED,
SOLANA_ERROR__RPC_WEBSOCKET_TRANSPORT_FAILED_TO_CONNECT,
SolanaError,
} from '@solana/errors';
import WS from 'jest-websocket-mock';
import { Client } from 'mock-socket';

Expand Down Expand Up @@ -38,7 +43,11 @@ describe('createWebSocketConnection', () => {
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);
Expand All @@ -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,
}),
);
});
});

Expand Down Expand Up @@ -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);
Expand All @@ -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),
);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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<{
Expand Down Expand Up @@ -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,
}),
);
}
}
Expand Down Expand Up @@ -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,
),
);
};
});
Expand Down Expand Up @@ -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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the reason for #2235.

});
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ export function getRpcSubscriptionsWithSubscriptionCoalescing<TRpcSubscriptionsM
>({
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export function getWebSocketTransportWithConnectionSharding<TTransport extends R
return getCachedAbortableIterableFactory({
getAbortSignalFromInputArgs: ({ signal }) => 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),
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading