@solana/errors 6.3.1 → 6.3.2-canary-20260313112147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +1 -1
- package/package.json +3 -2
- package/src/cli.ts +60 -0
- package/src/codes.ts +682 -0
- package/src/context.ts +862 -0
- package/src/error.ts +148 -0
- package/src/index.ts +72 -0
- package/src/instruction-error.ts +96 -0
- package/src/json-rpc-error.ts +162 -0
- package/src/message-formatter.ts +115 -0
- package/src/messages.ts +791 -0
- package/src/rpc-enum-errors.ts +53 -0
- package/src/simulation-errors.ts +47 -0
- package/src/stack-trace.ts +5 -0
- package/src/transaction-error.ts +94 -0
package/src/error.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { SolanaErrorCode, SolanaErrorCodeWithCause, SolanaErrorCodeWithDeprecatedCause } from './codes';
|
|
2
|
+
import { SolanaErrorContext } from './context';
|
|
3
|
+
import { getErrorMessage } from './message-formatter';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A variant of {@link SolanaError} where the `cause` property is deprecated.
|
|
7
|
+
*
|
|
8
|
+
* This type is returned by {@link isSolanaError} when checking for error codes in
|
|
9
|
+
* {@link SolanaErrorCodeWithDeprecatedCause}. Accessing `cause` on these errors will show
|
|
10
|
+
* a deprecation warning in IDEs that support JSDoc `@deprecated` tags.
|
|
11
|
+
*/
|
|
12
|
+
export interface SolanaErrorWithDeprecatedCause<
|
|
13
|
+
TErrorCode extends SolanaErrorCodeWithDeprecatedCause = SolanaErrorCodeWithDeprecatedCause,
|
|
14
|
+
> extends Omit<SolanaError<TErrorCode>, 'cause'> {
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated The `cause` property is deprecated for this error code.
|
|
17
|
+
* Use the error's `context` property instead to access relevant error information.
|
|
18
|
+
*/
|
|
19
|
+
readonly cause?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A type guard that returns `true` if the input is a {@link SolanaError}, optionally with a
|
|
24
|
+
* particular error code.
|
|
25
|
+
*
|
|
26
|
+
* When the `code` argument is supplied and the input is a {@link SolanaError}, TypeScript will
|
|
27
|
+
* refine the error's {@link SolanaError#context | `context`} property to the type associated with
|
|
28
|
+
* that error code. You can use that context to render useful error messages, or to make
|
|
29
|
+
* context-aware decisions that help your application to recover from the error.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import {
|
|
34
|
+
* SOLANA_ERROR__TRANSACTION__MISSING_SIGNATURE,
|
|
35
|
+
* SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING,
|
|
36
|
+
* isSolanaError,
|
|
37
|
+
* } from '@solana/errors';
|
|
38
|
+
* import { assertIsFullySignedTransaction, getSignatureFromTransaction } from '@solana/transactions';
|
|
39
|
+
*
|
|
40
|
+
* try {
|
|
41
|
+
* const transactionSignature = getSignatureFromTransaction(tx);
|
|
42
|
+
* assertIsFullySignedTransaction(tx);
|
|
43
|
+
* /* ... *\/
|
|
44
|
+
* } catch (e) {
|
|
45
|
+
* if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) {
|
|
46
|
+
* displayError(
|
|
47
|
+
* "We can't send this transaction without signatures for these addresses:\n- %s",
|
|
48
|
+
* // The type of the `context` object is now refined to contain `addresses`.
|
|
49
|
+
* e.context.addresses.join('\n- '),
|
|
50
|
+
* );
|
|
51
|
+
* return;
|
|
52
|
+
* } else if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING)) {
|
|
53
|
+
* if (!tx.feePayer) {
|
|
54
|
+
* displayError('Choose a fee payer for this transaction before sending it');
|
|
55
|
+
* } else {
|
|
56
|
+
* displayError('The fee payer still needs to sign for this transaction');
|
|
57
|
+
* }
|
|
58
|
+
* return;
|
|
59
|
+
* }
|
|
60
|
+
* throw e;
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function isSolanaError<TErrorCode extends SolanaErrorCodeWithDeprecatedCause>(
|
|
65
|
+
e: unknown,
|
|
66
|
+
code: TErrorCode,
|
|
67
|
+
): e is SolanaErrorWithDeprecatedCause<TErrorCode>;
|
|
68
|
+
export function isSolanaError<TErrorCode extends SolanaErrorCode>(
|
|
69
|
+
e: unknown,
|
|
70
|
+
code?: TErrorCode,
|
|
71
|
+
): e is SolanaError<TErrorCode>;
|
|
72
|
+
export function isSolanaError<TErrorCode extends SolanaErrorCode>(
|
|
73
|
+
e: unknown,
|
|
74
|
+
/**
|
|
75
|
+
* When supplied, this function will require that the input is a {@link SolanaError} _and_ that
|
|
76
|
+
* its error code is exactly this value.
|
|
77
|
+
*/
|
|
78
|
+
code?: TErrorCode,
|
|
79
|
+
): e is SolanaError<TErrorCode> {
|
|
80
|
+
const isSolanaError = e instanceof Error && e.name === 'SolanaError';
|
|
81
|
+
if (isSolanaError) {
|
|
82
|
+
if (code !== undefined) {
|
|
83
|
+
return (e as SolanaError<TErrorCode>).context.__code === code;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type SolanaErrorCodedContext = {
|
|
91
|
+
[P in SolanaErrorCode]: Readonly<{
|
|
92
|
+
__code: P;
|
|
93
|
+
}> &
|
|
94
|
+
(SolanaErrorContext[P] extends undefined ? object : SolanaErrorContext[P]);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Encapsulates an error's stacktrace, a Solana-specific numeric code that indicates what went
|
|
99
|
+
* wrong, and optional context if the type of error indicated by the code supports it.
|
|
100
|
+
*/
|
|
101
|
+
export class SolanaError<TErrorCode extends SolanaErrorCode = SolanaErrorCode> extends Error {
|
|
102
|
+
/**
|
|
103
|
+
* Indicates the root cause of this {@link SolanaError}, if any.
|
|
104
|
+
*
|
|
105
|
+
* For example, a transaction error might have an instruction error as its root cause. In this
|
|
106
|
+
* case, you will be able to access the instruction error on the transaction error as `cause`.
|
|
107
|
+
*/
|
|
108
|
+
readonly cause?: TErrorCode extends SolanaErrorCodeWithCause ? SolanaError : unknown = this.cause;
|
|
109
|
+
/**
|
|
110
|
+
* Contains context that can assist in understanding or recovering from a {@link SolanaError}.
|
|
111
|
+
*/
|
|
112
|
+
readonly context: SolanaErrorCodedContext[TErrorCode];
|
|
113
|
+
constructor(
|
|
114
|
+
...[code, contextAndErrorOptions]: SolanaErrorContext[TErrorCode] extends undefined
|
|
115
|
+
? [code: TErrorCode, errorOptions?: ErrorOptions | undefined]
|
|
116
|
+
: [code: TErrorCode, contextAndErrorOptions: SolanaErrorContext[TErrorCode] & (ErrorOptions | undefined)]
|
|
117
|
+
) {
|
|
118
|
+
let context: SolanaErrorContext[TErrorCode] | undefined;
|
|
119
|
+
let errorOptions: ErrorOptions | undefined;
|
|
120
|
+
if (contextAndErrorOptions) {
|
|
121
|
+
Object.entries(Object.getOwnPropertyDescriptors(contextAndErrorOptions)).forEach(([name, descriptor]) => {
|
|
122
|
+
// If the `ErrorOptions` type ever changes, update this code.
|
|
123
|
+
if (name === 'cause') {
|
|
124
|
+
errorOptions = { cause: descriptor.value };
|
|
125
|
+
} else {
|
|
126
|
+
if (context === undefined) {
|
|
127
|
+
context = {
|
|
128
|
+
__code: code,
|
|
129
|
+
} as unknown as SolanaErrorContext[TErrorCode];
|
|
130
|
+
}
|
|
131
|
+
Object.defineProperty(context, name, descriptor);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const message = getErrorMessage(code, context);
|
|
136
|
+
super(message, errorOptions);
|
|
137
|
+
this.context = Object.freeze(
|
|
138
|
+
context === undefined
|
|
139
|
+
? {
|
|
140
|
+
__code: code,
|
|
141
|
+
}
|
|
142
|
+
: context,
|
|
143
|
+
) as SolanaErrorCodedContext[TErrorCode];
|
|
144
|
+
// This is necessary so that `isSolanaError()` can identify a `SolanaError` without having
|
|
145
|
+
// to import the class for use in an `instanceof` check.
|
|
146
|
+
this.name = 'SolanaError';
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This package brings together every error message across all Solana JavaScript modules.
|
|
3
|
+
*
|
|
4
|
+
* # Reading error messages
|
|
5
|
+
*
|
|
6
|
+
* ## In development mode
|
|
7
|
+
*
|
|
8
|
+
* When your bundler sets the constant `__DEV__` to `true`, every error message will be included in
|
|
9
|
+
* the bundle. As such, you will be able to read them in plain language wherever they appear.
|
|
10
|
+
*
|
|
11
|
+
* > [!WARNING]
|
|
12
|
+
* > The size of your JavaScript bundle will increase significantly with the inclusion of every
|
|
13
|
+
* > error message in development mode. Be sure to build your bundle with `__DEV__` set to `false`
|
|
14
|
+
* > when you go to production.
|
|
15
|
+
*
|
|
16
|
+
* ## In production mode
|
|
17
|
+
*
|
|
18
|
+
* When your bundler sets the constant `__DEV__` to `false`, error messages will be stripped from
|
|
19
|
+
* the bundle to save space. Only the error code will appear when an error is encountered. Follow
|
|
20
|
+
* the instructions in the error message to convert the error code back to the human-readable error
|
|
21
|
+
* message.
|
|
22
|
+
*
|
|
23
|
+
* For instance, to recover the error text for the error with code `123`:
|
|
24
|
+
*
|
|
25
|
+
* ```package-install
|
|
26
|
+
* npx @solana/errors decode -- 123
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* > [!IMPORTANT]
|
|
30
|
+
* > The string representation of a {@link SolanaError} should not be shown to users. Developers
|
|
31
|
+
* > should use {@link isSolanaError} to distinguish the type of a thrown error, then show a custom,
|
|
32
|
+
* > localized error message appropriate for their application's audience. Custom error messages
|
|
33
|
+
* > should use the error's {@link SolanaError#context | `context`} if it would help the reader
|
|
34
|
+
* > understand what happened and/or what to do next.
|
|
35
|
+
*
|
|
36
|
+
* # Adding a new error
|
|
37
|
+
*
|
|
38
|
+
* 1. Add a new exported error code constant to `src/codes.ts`.
|
|
39
|
+
* 2. Add that new constant to the {@link SolanaErrorCode} union in `src/codes.ts`.
|
|
40
|
+
* 3. If you would like the new error to encapsulate context about the error itself (eg. the public
|
|
41
|
+
* keys for which a transaction is missing signatures) define the shape of that context in
|
|
42
|
+
* `src/context.ts`.
|
|
43
|
+
* 4. Add the error's message to `src/messages.ts`. Any context values that you defined above will
|
|
44
|
+
* be interpolated into the message wherever you write `$key`, where `key` is the index of a
|
|
45
|
+
* value in the context (eg. ``'Missing a signature for account `$address`'``).
|
|
46
|
+
* 5. Publish a new version of `@solana/errors`.
|
|
47
|
+
* 6. Bump the version of `@solana/errors` in the package from which the error is thrown.
|
|
48
|
+
*
|
|
49
|
+
* # Removing an error message
|
|
50
|
+
*
|
|
51
|
+
* - Don't remove errors.
|
|
52
|
+
* - Don't change the meaning of an error message.
|
|
53
|
+
* - Don't change or reorder error codes.
|
|
54
|
+
* - Don't change or remove members of an error's context.
|
|
55
|
+
*
|
|
56
|
+
* When an older client throws an error, we want to make sure that they can always decode the error.
|
|
57
|
+
* If you make any of the changes above, old clients will, by definition, not have received your
|
|
58
|
+
* changes. This could make the errors that they throw impossible to decode going forward.
|
|
59
|
+
*
|
|
60
|
+
* # Catching errors
|
|
61
|
+
*
|
|
62
|
+
* See {@link isSolanaError} for an example of how to handle a caught {@link SolanaError}.
|
|
63
|
+
*
|
|
64
|
+
* @packageDocumentation
|
|
65
|
+
*/
|
|
66
|
+
export * from './codes';
|
|
67
|
+
export * from './error';
|
|
68
|
+
export * from './instruction-error';
|
|
69
|
+
export * from './json-rpc-error';
|
|
70
|
+
export * from './simulation-errors';
|
|
71
|
+
export * from './stack-trace';
|
|
72
|
+
export * from './transaction-error';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN } from './codes';
|
|
2
|
+
import { SolanaError } from './error';
|
|
3
|
+
import { getSolanaErrorFromRpcError } from './rpc-enum-errors';
|
|
4
|
+
|
|
5
|
+
const ORDERED_ERROR_NAMES = [
|
|
6
|
+
// Keep synced with RPC source: https://github.com/anza-xyz/solana-sdk/blob/master/instruction-error/src/lib.rs
|
|
7
|
+
// If this list ever gets too large, consider implementing a compression strategy like this:
|
|
8
|
+
// https://gist.github.com/steveluscher/aaa7cbbb5433b1197983908a40860c47
|
|
9
|
+
'GenericError',
|
|
10
|
+
'InvalidArgument',
|
|
11
|
+
'InvalidInstructionData',
|
|
12
|
+
'InvalidAccountData',
|
|
13
|
+
'AccountDataTooSmall',
|
|
14
|
+
'InsufficientFunds',
|
|
15
|
+
'IncorrectProgramId',
|
|
16
|
+
'MissingRequiredSignature',
|
|
17
|
+
'AccountAlreadyInitialized',
|
|
18
|
+
'UninitializedAccount',
|
|
19
|
+
'UnbalancedInstruction',
|
|
20
|
+
'ModifiedProgramId',
|
|
21
|
+
'ExternalAccountLamportSpend',
|
|
22
|
+
'ExternalAccountDataModified',
|
|
23
|
+
'ReadonlyLamportChange',
|
|
24
|
+
'ReadonlyDataModified',
|
|
25
|
+
'DuplicateAccountIndex',
|
|
26
|
+
'ExecutableModified',
|
|
27
|
+
'RentEpochModified',
|
|
28
|
+
'NotEnoughAccountKeys',
|
|
29
|
+
'AccountDataSizeChanged',
|
|
30
|
+
'AccountNotExecutable',
|
|
31
|
+
'AccountBorrowFailed',
|
|
32
|
+
'AccountBorrowOutstanding',
|
|
33
|
+
'DuplicateAccountOutOfSync',
|
|
34
|
+
'Custom',
|
|
35
|
+
'InvalidError',
|
|
36
|
+
'ExecutableDataModified',
|
|
37
|
+
'ExecutableLamportChange',
|
|
38
|
+
'ExecutableAccountNotRentExempt',
|
|
39
|
+
'UnsupportedProgramId',
|
|
40
|
+
'CallDepth',
|
|
41
|
+
'MissingAccount',
|
|
42
|
+
'ReentrancyNotAllowed',
|
|
43
|
+
'MaxSeedLengthExceeded',
|
|
44
|
+
'InvalidSeeds',
|
|
45
|
+
'InvalidRealloc',
|
|
46
|
+
'ComputationalBudgetExceeded',
|
|
47
|
+
'PrivilegeEscalation',
|
|
48
|
+
'ProgramEnvironmentSetupFailure',
|
|
49
|
+
'ProgramFailedToComplete',
|
|
50
|
+
'ProgramFailedToCompile',
|
|
51
|
+
'Immutable',
|
|
52
|
+
'IncorrectAuthority',
|
|
53
|
+
'BorshIoError',
|
|
54
|
+
'AccountNotRentExempt',
|
|
55
|
+
'InvalidAccountOwner',
|
|
56
|
+
'ArithmeticOverflow',
|
|
57
|
+
'UnsupportedSysvar',
|
|
58
|
+
'IllegalOwner',
|
|
59
|
+
'MaxAccountsDataAllocationsExceeded',
|
|
60
|
+
'MaxAccountsExceeded',
|
|
61
|
+
'MaxInstructionTraceLengthExceeded',
|
|
62
|
+
'BuiltinProgramsMustConsumeComputeUnits',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export function getSolanaErrorFromInstructionError(
|
|
66
|
+
/**
|
|
67
|
+
* The index of the instruction inside the transaction.
|
|
68
|
+
*/
|
|
69
|
+
index: bigint | number,
|
|
70
|
+
instructionError: string | { [key: string]: unknown },
|
|
71
|
+
): SolanaError {
|
|
72
|
+
const numberIndex = Number(index);
|
|
73
|
+
return getSolanaErrorFromRpcError(
|
|
74
|
+
{
|
|
75
|
+
errorCodeBaseOffset: 4615001,
|
|
76
|
+
getErrorContext(errorCode, rpcErrorName, rpcErrorContext) {
|
|
77
|
+
if (errorCode === SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN) {
|
|
78
|
+
return {
|
|
79
|
+
errorName: rpcErrorName,
|
|
80
|
+
index: numberIndex,
|
|
81
|
+
...(rpcErrorContext !== undefined ? { instructionErrorContext: rpcErrorContext } : null),
|
|
82
|
+
};
|
|
83
|
+
} else if (errorCode === SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM) {
|
|
84
|
+
return {
|
|
85
|
+
code: Number(rpcErrorContext as bigint | number),
|
|
86
|
+
index: numberIndex,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { index: numberIndex };
|
|
90
|
+
},
|
|
91
|
+
orderedErrorNames: ORDERED_ERROR_NAMES,
|
|
92
|
+
rpcEnumError: instructionError,
|
|
93
|
+
},
|
|
94
|
+
getSolanaErrorFromInstructionError,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR,
|
|
3
|
+
SOLANA_ERROR__JSON_RPC__INVALID_PARAMS,
|
|
4
|
+
SOLANA_ERROR__JSON_RPC__INVALID_REQUEST,
|
|
5
|
+
SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND,
|
|
6
|
+
SOLANA_ERROR__JSON_RPC__PARSE_ERROR,
|
|
7
|
+
SOLANA_ERROR__JSON_RPC__SCAN_ERROR,
|
|
8
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP,
|
|
9
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE,
|
|
10
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET,
|
|
11
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX,
|
|
12
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED,
|
|
13
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
|
|
14
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED,
|
|
15
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE,
|
|
16
|
+
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION,
|
|
17
|
+
SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR,
|
|
18
|
+
SolanaErrorCode,
|
|
19
|
+
} from './codes';
|
|
20
|
+
import { SolanaErrorContext } from './context';
|
|
21
|
+
import { SolanaError } from './error';
|
|
22
|
+
import { safeCaptureStackTrace } from './stack-trace';
|
|
23
|
+
import { getSolanaErrorFromTransactionError } from './transaction-error';
|
|
24
|
+
|
|
25
|
+
interface RpcErrorResponse {
|
|
26
|
+
code: bigint | number;
|
|
27
|
+
data?: unknown;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type TransactionError = string | { [key: string]: unknown };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Keep in sync with https://github.com/anza-xyz/agave/blob/master/rpc-client-types/src/response.rs
|
|
35
|
+
* @hidden
|
|
36
|
+
*/
|
|
37
|
+
export interface RpcSimulateTransactionResult {
|
|
38
|
+
accounts:
|
|
39
|
+
| ({
|
|
40
|
+
data:
|
|
41
|
+
| string // LegacyBinary
|
|
42
|
+
| {
|
|
43
|
+
// Json
|
|
44
|
+
parsed: unknown;
|
|
45
|
+
program: string;
|
|
46
|
+
space: bigint;
|
|
47
|
+
}
|
|
48
|
+
// Binary
|
|
49
|
+
| [encodedBytes: string, encoding: 'base58' | 'base64' | 'base64+zstd' | 'binary' | 'jsonParsed'];
|
|
50
|
+
executable: boolean;
|
|
51
|
+
lamports: bigint;
|
|
52
|
+
owner: string;
|
|
53
|
+
rentEpoch: bigint;
|
|
54
|
+
space?: bigint;
|
|
55
|
+
} | null)[]
|
|
56
|
+
| null;
|
|
57
|
+
err: TransactionError | null;
|
|
58
|
+
// Enabled by `enable_cpi_recording`
|
|
59
|
+
innerInstructions?:
|
|
60
|
+
| {
|
|
61
|
+
index: number;
|
|
62
|
+
instructions: (
|
|
63
|
+
| {
|
|
64
|
+
// Compiled
|
|
65
|
+
accounts: number[];
|
|
66
|
+
data: string;
|
|
67
|
+
programIdIndex: number;
|
|
68
|
+
stackHeight?: number;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
// Parsed
|
|
72
|
+
parsed: unknown;
|
|
73
|
+
program: string;
|
|
74
|
+
programId: string;
|
|
75
|
+
stackHeight?: number;
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
// PartiallyDecoded
|
|
79
|
+
accounts: string[];
|
|
80
|
+
data: string;
|
|
81
|
+
programId: string;
|
|
82
|
+
stackHeight?: number;
|
|
83
|
+
}
|
|
84
|
+
)[];
|
|
85
|
+
}[]
|
|
86
|
+
| null;
|
|
87
|
+
loadedAccountsDataSize: number | null;
|
|
88
|
+
logs: string[] | null;
|
|
89
|
+
replacementBlockhash: string | null;
|
|
90
|
+
returnData: {
|
|
91
|
+
data: [string, 'base64'];
|
|
92
|
+
programId: string;
|
|
93
|
+
} | null;
|
|
94
|
+
unitsConsumed: bigint | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getSolanaErrorFromJsonRpcError(putativeErrorResponse: unknown): SolanaError {
|
|
98
|
+
let out: SolanaError;
|
|
99
|
+
if (isRpcErrorResponse(putativeErrorResponse)) {
|
|
100
|
+
const { code: rawCode, data, message } = putativeErrorResponse;
|
|
101
|
+
const code = Number(rawCode);
|
|
102
|
+
if (code === SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE) {
|
|
103
|
+
const { err, ...preflightErrorContext } = data as RpcSimulateTransactionResult;
|
|
104
|
+
const causeObject = err ? { cause: getSolanaErrorFromTransactionError(err) } : null;
|
|
105
|
+
out = new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, {
|
|
106
|
+
...preflightErrorContext,
|
|
107
|
+
...causeObject,
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
let errorContext;
|
|
111
|
+
switch (code) {
|
|
112
|
+
case SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR:
|
|
113
|
+
case SOLANA_ERROR__JSON_RPC__INVALID_PARAMS:
|
|
114
|
+
case SOLANA_ERROR__JSON_RPC__INVALID_REQUEST:
|
|
115
|
+
case SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND:
|
|
116
|
+
case SOLANA_ERROR__JSON_RPC__PARSE_ERROR:
|
|
117
|
+
case SOLANA_ERROR__JSON_RPC__SCAN_ERROR:
|
|
118
|
+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP:
|
|
119
|
+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE:
|
|
120
|
+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET:
|
|
121
|
+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX:
|
|
122
|
+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED:
|
|
123
|
+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED:
|
|
124
|
+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE:
|
|
125
|
+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION:
|
|
126
|
+
// The server supplies no structured data, but rather a pre-formatted message. Put
|
|
127
|
+
// the server message in `context` so as not to completely lose the data. The long
|
|
128
|
+
// term fix for this is to add data to the server responses and modify the
|
|
129
|
+
// messages in `@solana/errors` to be actual format strings.
|
|
130
|
+
errorContext = { __serverMessage: message };
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
if (typeof data === 'object' && !Array.isArray(data)) {
|
|
134
|
+
errorContext = data;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
out = new SolanaError(code as SolanaErrorCode, errorContext as SolanaErrorContext[SolanaErrorCode]);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
const message =
|
|
141
|
+
typeof putativeErrorResponse === 'object' &&
|
|
142
|
+
putativeErrorResponse !== null &&
|
|
143
|
+
'message' in putativeErrorResponse &&
|
|
144
|
+
typeof putativeErrorResponse.message === 'string'
|
|
145
|
+
? putativeErrorResponse.message
|
|
146
|
+
: 'Malformed JSON-RPC error with no message attribute';
|
|
147
|
+
out = new SolanaError(SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR, { error: putativeErrorResponse, message });
|
|
148
|
+
}
|
|
149
|
+
safeCaptureStackTrace(out, getSolanaErrorFromJsonRpcError);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isRpcErrorResponse(value: unknown): value is RpcErrorResponse {
|
|
154
|
+
return (
|
|
155
|
+
typeof value === 'object' &&
|
|
156
|
+
value !== null &&
|
|
157
|
+
'code' in value &&
|
|
158
|
+
'message' in value &&
|
|
159
|
+
(typeof value.code === 'number' || typeof value.code === 'bigint') &&
|
|
160
|
+
typeof value.message === 'string'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN, SolanaErrorCode } from './codes';
|
|
2
|
+
import { encodeContextObject } from './context';
|
|
3
|
+
import { SolanaErrorMessages } from './messages';
|
|
4
|
+
|
|
5
|
+
const INSTRUCTION_ERROR_RANGE_SIZE = 1000;
|
|
6
|
+
|
|
7
|
+
const enum StateType {
|
|
8
|
+
EscapeSequence,
|
|
9
|
+
Text,
|
|
10
|
+
Variable,
|
|
11
|
+
}
|
|
12
|
+
type State = Readonly<{
|
|
13
|
+
[START_INDEX]: number;
|
|
14
|
+
[TYPE]: StateType;
|
|
15
|
+
}>;
|
|
16
|
+
const START_INDEX = 'i';
|
|
17
|
+
const TYPE = 't';
|
|
18
|
+
|
|
19
|
+
export function getHumanReadableErrorMessage<TErrorCode extends SolanaErrorCode>(
|
|
20
|
+
code: TErrorCode,
|
|
21
|
+
context: object = {},
|
|
22
|
+
): string {
|
|
23
|
+
const messageFormatString = SolanaErrorMessages[code];
|
|
24
|
+
if (messageFormatString.length === 0) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
let state: State;
|
|
28
|
+
function commitStateUpTo(endIndex?: number) {
|
|
29
|
+
if (state[TYPE] === StateType.Variable) {
|
|
30
|
+
const variableName = messageFormatString.slice(state[START_INDEX] + 1, endIndex);
|
|
31
|
+
|
|
32
|
+
fragments.push(
|
|
33
|
+
variableName in context
|
|
34
|
+
? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
35
|
+
`${context[variableName as keyof typeof context]}`
|
|
36
|
+
: `$${variableName}`,
|
|
37
|
+
);
|
|
38
|
+
} else if (state[TYPE] === StateType.Text) {
|
|
39
|
+
fragments.push(messageFormatString.slice(state[START_INDEX], endIndex));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const fragments: string[] = [];
|
|
43
|
+
messageFormatString.split('').forEach((char, ii) => {
|
|
44
|
+
if (ii === 0) {
|
|
45
|
+
state = {
|
|
46
|
+
[START_INDEX]: 0,
|
|
47
|
+
[TYPE]:
|
|
48
|
+
messageFormatString[0] === '\\'
|
|
49
|
+
? StateType.EscapeSequence
|
|
50
|
+
: messageFormatString[0] === '$'
|
|
51
|
+
? StateType.Variable
|
|
52
|
+
: StateType.Text,
|
|
53
|
+
};
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let nextState;
|
|
57
|
+
switch (state[TYPE]) {
|
|
58
|
+
case StateType.EscapeSequence:
|
|
59
|
+
nextState = { [START_INDEX]: ii, [TYPE]: StateType.Text };
|
|
60
|
+
break;
|
|
61
|
+
case StateType.Text:
|
|
62
|
+
if (char === '\\') {
|
|
63
|
+
nextState = { [START_INDEX]: ii, [TYPE]: StateType.EscapeSequence };
|
|
64
|
+
} else if (char === '$') {
|
|
65
|
+
nextState = { [START_INDEX]: ii, [TYPE]: StateType.Variable };
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case StateType.Variable:
|
|
69
|
+
if (char === '\\') {
|
|
70
|
+
nextState = { [START_INDEX]: ii, [TYPE]: StateType.EscapeSequence };
|
|
71
|
+
} else if (char === '$') {
|
|
72
|
+
nextState = { [START_INDEX]: ii, [TYPE]: StateType.Variable };
|
|
73
|
+
} else if (!char.match(/\w/)) {
|
|
74
|
+
nextState = { [START_INDEX]: ii, [TYPE]: StateType.Text };
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
if (nextState) {
|
|
79
|
+
if (state !== nextState) {
|
|
80
|
+
commitStateUpTo(ii);
|
|
81
|
+
}
|
|
82
|
+
state = nextState;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
commitStateUpTo();
|
|
86
|
+
let message = fragments.join('');
|
|
87
|
+
if (
|
|
88
|
+
code >= SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN &&
|
|
89
|
+
code < SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN + INSTRUCTION_ERROR_RANGE_SIZE &&
|
|
90
|
+
'index' in context
|
|
91
|
+
) {
|
|
92
|
+
message += ` (instruction #${(context as { index: number }).index + 1})`;
|
|
93
|
+
}
|
|
94
|
+
return message;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getErrorMessage<TErrorCode extends SolanaErrorCode>(
|
|
98
|
+
code: TErrorCode,
|
|
99
|
+
context: Record<string, unknown> = {},
|
|
100
|
+
): string {
|
|
101
|
+
if (__DEV__) {
|
|
102
|
+
return getHumanReadableErrorMessage(code, context);
|
|
103
|
+
} else {
|
|
104
|
+
let decodingAdviceMessage = `Solana error #${code}; Decode this error by running \`npx @solana/errors decode -- ${code}`;
|
|
105
|
+
if (Object.keys(context).length) {
|
|
106
|
+
/**
|
|
107
|
+
* DANGER: Be sure that the shell command is escaped in such a way that makes it
|
|
108
|
+
* impossible for someone to craft malicious context values that would result in
|
|
109
|
+
* an exploit against anyone who bindly copy/pastes it into their terminal.
|
|
110
|
+
*/
|
|
111
|
+
decodingAdviceMessage += ` '${encodeContextObject(context)}'`;
|
|
112
|
+
}
|
|
113
|
+
return `${decodingAdviceMessage}\``;
|
|
114
|
+
}
|
|
115
|
+
}
|