Skip to content

Commit

Permalink
Convert all errors in @solana/rpc-subscriptions-* to coded exceptio…
Browse files Browse the repository at this point in the history
…ns (#2236)
  • Loading branch information
steveluscher committed Mar 4, 2024
1 parent 803b2d8 commit cf9c20c
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 29 deletions.
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,
});
}
}
}
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.

0 comments on commit cf9c20c

Please sign in to comment.