Skip to content

Commit

Permalink
Document @solana/errors with TypeDoc
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Jan 17, 2025
1 parent 5644fd5 commit 0433a03
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 13 deletions.
6 changes: 5 additions & 1 deletion packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors/#adding-a-new-error
*
* @module
* @privateRemarks
* WARNING:
* - Don't remove error codes
* - Don't change or reorder error codes.
Expand Down Expand Up @@ -302,6 +304,7 @@ export const SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEME
/**
* A union of every Solana error code
*
* @privateRemarks
* You might be wondering why this is not a TypeScript enum or const enum.
*
* One of the goals of this library is to enable people to use some or none of it without having to
Expand Down Expand Up @@ -541,6 +544,7 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_VOTE_COST_LIMIT;

/**
* Errors of this type are understood to have an optional `SolanaError` nested inside as `cause`.
* Errors of this type are understood to have an optional {@link SolanaError} nested inside as
* `cause`.
*/
export type SolanaErrorCodeWithCause = typeof SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE;
14 changes: 9 additions & 5 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors/#adding-a-new-error
*
* @privateRemarks
* WARNING:
* - Don't change or remove members of an error's context.
*/
import {
SOLANA_ERROR__ACCOUNTS__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__ACCOUNTS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED,
Expand Down Expand Up @@ -167,11 +175,7 @@ interface ReadonlyUint8Array extends Omit<Uint8Array, TypedArrayMutablePropertie
}

/**
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors/#adding-a-new-error
*
* WARNING:
* - Don't change or remove members of an error's context.
* A map of every {@link SolanaError} code to the type of its `context` property.
*/
export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
BasicInstructionErrorContext<
Expand Down
59 changes: 59 additions & 0 deletions packages/errors/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,54 @@ import { SolanaErrorCode, SolanaErrorCodeWithCause } from './codes';
import { SolanaErrorContext } from './context';
import { getErrorMessage } from './message-formatter';

/**
* A type guard that returns `true` if the input is a {@link SolanaError}, optionally with a
* particular error code.
*
* When the `code` argument is supplied and the input is a {@link SolanaError}, TypeScript will
* refine the error's {@link SolanaError#context | `context`} property to the type associated with
* that error code. You can use that context to render useful error messages, or to make
* context-aware decisions that help your application to recover from the error.
*
* @example
* ```ts
* import {
* SOLANA_ERROR__TRANSACTION__MISSING_SIGNATURE,
* SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING,
* isSolanaError,
* } from '@solana/errors';
* import { assertTransactionIsFullySigned, getSignatureFromTransaction } from '@solana/transactions';
*
* try {
* const transactionSignature = getSignatureFromTransaction(tx);
* assertTransactionIsFullySigned(tx);
* /* ... *\/
* } catch (e) {
* if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) {
* displayError(
* "We can't send this transaction without signatures for these addresses:\n- %s",
* // The type of the `context` object is now refined to contain `addresses`.
* e.context.addresses.join('\n- '),
* );
* return;
* } else if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING)) {
* if (!tx.feePayer) {
* displayError('Choose a fee payer for this transaction before sending it');
* } else {
* displayError('The fee payer still needs to sign for this transaction');
* }
* return;
* }
* throw e;
* }
* ```
*/
export function isSolanaError<TErrorCode extends SolanaErrorCode>(
e: unknown,
/**
* When supplied, this function will require that the input is a {@link SolanaError} _and_ that
* its error code is exactly this value.
*/
code?: TErrorCode,
): e is SolanaError<TErrorCode> {
const isSolanaError = e instanceof Error && e.name === 'SolanaError';
Expand All @@ -22,8 +68,21 @@ type SolanaErrorCodedContext = Readonly<{
};
}>;

/**
* Encapsulates an error's stacktrace, a Solana-specific numeric code that indicates what went
* wrong, and optional context if the type of error indicated by the code supports it.
*/
export class SolanaError<TErrorCode extends SolanaErrorCode = SolanaErrorCode> extends Error {
/**
* Indicates the root cause of this {@link SolanaError}, if any.
*
* For example, a transaction error might have an instruction error as its root cause. In this
* case, you will be able to access the instruction error on the transaction error as `cause`.
*/
readonly cause?: TErrorCode extends SolanaErrorCodeWithCause ? SolanaError : unknown = this.cause;
/**
* Contains context that can assist in understanding or recovering from a {@link SolanaError}.
*/
readonly context: SolanaErrorCodedContext[TErrorCode];
constructor(
...[code, contextAndErrorOptions]: SolanaErrorContext[TErrorCode] extends undefined
Expand Down
65 changes: 65 additions & 0 deletions packages/errors/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
/**
* This package brings together every error message across all Solana JavaScript modules.
*
* # Reading error messages
*
* ## In development mode
*
* When your bundler sets the constant `__DEV__` to `true`, every error message will be included in
* the bundle. As such, you will be able to read them in plain language wherever they appear.
*
* > [!WARNING]
* > The size of your JavaScript bundle will increase significantly with the inclusion of every
* > error message in development mode. Be sure to build your bundle with `__DEV__` set to `false`
* > when you go to production.
*
* ## In production mode
*
* When your bundler sets the constant `__DEV__` to `false`, error messages will be stripped from
* the bundle to save space. Only the error code will appear when an error is encountered. Follow
* the instructions in the error message to convert the error code back to the human-readable error
* message.
*
* For instance, to recover the error text for the error with code `123`:
*
* ```package-install
* npx @solana/errors decode -- 123
* ```
*
* > [!IMPORTANT]
* > The string representation of a {@link SolanaError} should not be shown to users. Developers
* > should use {@link isSolanaError} to distinguish the type of a thrown error, then show a custom,
* > localized error message appropriate for their application's audience. Custom error messages
* > should use the error's {@link SolanaError#context | `context`} if it would help the reader
* > understand what happened and/or what to do next.
*
* # Adding a new error
*
* 1. Add a new exported error code constant to `src/codes.ts`.
* 2. Add that new constant to the {@link SolanaErrorCode} union in `src/codes.ts`.
* 3. If you would like the new error to encapsulate context about the error itself (eg. the public
* keys for which a transaction is missing signatures) define the shape of that context in
* `src/context.ts`.
* 4. Add the error's message to `src/messages.ts`. Any context values that you defined above will
* be interpolated into the message wherever you write `$key`, where `key` is the index of a
* value in the context (eg. ``'Missing a signature for account `$address`'``).
* 5. Publish a new version of `@solana/errors`.
* 6. Bump the version of `@solana/errors` in the package from which the error is thrown.
*
* # Removing an error message
*
* - Don't remove errors.
* - Don't change the meaning of an error message.
* - Don't change or reorder error codes.
* - Don't change or remove members of an error's context.
*
* When an older client throws an error, we want to make sure that they can always decode the error.
* If you make any of the changes above, old clients will, by definition, not have received your
* changes. This could make the errors that they throw impossible to decode going forward.
*
* # Catching errors
*
* See {@link isSolanaError} for an example of how to handle a caught {@link SolanaError}.
*
* @packageDocumentation
*/
export * from './codes';
export * from './error';
export * from './json-rpc-error';
Expand Down
3 changes: 3 additions & 0 deletions packages/errors/src/instruction-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ const ORDERED_ERROR_NAMES = [
];

export function getSolanaErrorFromInstructionError(
/**
* The index of the instruction inside the transaction.
*/
index: bigint | number,
instructionError: string | { [key: string]: unknown },
): SolanaError {
Expand Down
5 changes: 4 additions & 1 deletion packages/errors/src/json-rpc-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ interface RpcErrorResponse {

type TransactionError = string | { [key: string]: unknown };

// Keep in sync with https://github.com/anza-xyz/agave/blob/master/rpc-client-api/src/response.rs
/**
* Keep in sync with https://github.com/anza-xyz/agave/blob/master/rpc-client-api/src/response.rs
* @hidden
*/
export interface RpcSimulateTransactionResult {
accounts:
| ({
Expand Down
14 changes: 9 additions & 5 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors#adding-a-new-error
*
* WARNING:
* - Don't change the meaning of an error message.
*/
import {
SOLANA_ERROR__ACCOUNTS__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__ACCOUNTS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED,
Expand Down Expand Up @@ -227,11 +234,8 @@ import {
} from './codes';

/**
* To add a new error, follow the instructions at
* https://github.com/anza-xyz/solana-web3.js/tree/main/packages/errors#adding-a-new-error
*
* WARNING:
* - Don't change the meaning of an error message.
* A map of every {@link SolanaError} code to the error message shown to developers in development
* mode.
*/
export const SolanaErrorMessages: Readonly<{
// This type makes this data structure exhaustive with respect to `SolanaErrorCode`.
Expand Down
3 changes: 2 additions & 1 deletion packages/errors/typedoc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "https://typedoc.org/schema.json",
"extends": ["../typedoc.base.json"],
"entryPoints": ["src/index.ts"]
"entryPoints": ["src/index.ts"],
"readme": "none"
}

0 comments on commit 0433a03

Please sign in to comment.