Skip to content

Commit

Permalink
refactor(experimental): errors: accounts package (#2186)
Browse files Browse the repository at this point in the history
Adds custom `SolanaError` throws to the `@solana/accounts` package.
  • Loading branch information
buffalojoec committed Feb 29, 2024
1 parent 70cc8b2 commit 546263e
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 18 deletions.
1 change: 1 addition & 0 deletions packages/accounts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
],
"dependencies": {
"@solana/addresses": "workspace:*",
"@solana/errors": "workspace:*",
"@solana/codecs-core": "workspace:*",
"@solana/codecs-strings": "workspace:*",
"@solana/rpc-spec": "workspace:*",
Expand Down
17 changes: 15 additions & 2 deletions packages/accounts/src/__tests__/decode-account-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import '@solana/test-matchers/toBeFrozenObject';

import { Address } from '@solana/addresses';
import {
SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT,
SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED,
SolanaError,
} from '@solana/errors';

import { Account, EncodedAccount } from '../account';
import { assertAccountDecoded, assertAccountsDecoded, decodeAccount } from '../decode-account';
Expand Down Expand Up @@ -86,7 +91,11 @@ describe('assertDecodedAccount', () => {
const fn = () => assertAccountDecoded(account);

// Then we expect an error to be thrown
expect(fn).toThrow('Expected account [1111] to be decoded.');
expect(fn).toThrow(
new SolanaError(SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT, {
address: account.address,
}),
);
});

it('does not throw if the provided account is decoded', () => {
Expand Down Expand Up @@ -142,7 +151,11 @@ describe('assertDecodedAccounts', () => {
const fn = () => assertAccountsDecoded(accounts);

// Then we expect an error to be thrown
expect(fn).toThrow('Expected accounts [1111, 2222] to be decoded.');
expect(fn).toThrow(
new SolanaError(SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED, {
addresses: [accounts[0].address, accounts[1].address],
}),
);
});

it('does not throw if all of the provided accounts are decoded', () => {
Expand Down
18 changes: 16 additions & 2 deletions packages/accounts/src/__tests__/maybe-account-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import '@solana/test-matchers/toBeFrozenObject';

import {
SOLANA_ERROR__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND,
SolanaError,
} from '@solana/errors';

import { assertAccountExists, assertAccountsExist, MaybeEncodedAccount } from '../maybe-account';

describe('assertAccountExists', () => {
Expand All @@ -11,7 +17,11 @@ describe('assertAccountExists', () => {
const fn = () => assertAccountExists(maybeAccount);

// Then we expect an error to be thrown.
expect(fn).toThrow(`Expected account [1111] to exist`);
expect(fn).toThrow(
new SolanaError(SOLANA_ERROR__ACCOUNT_NOT_FOUND, {
address: maybeAccount.address,
}),
);
});
});

Expand All @@ -28,7 +38,11 @@ describe('assertAccountsExist', () => {
const fn = () => assertAccountsExist(maybeAccounts);

// Then we expect an error to be thrown with the non-existent accounts
expect(fn).toThrow('Expected accounts [1111, 2222] to exist');
expect(fn).toThrow(
new SolanaError(SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND, {
addresses: [maybeAccounts[0].address, maybeAccounts[1].address],
}),
);
});

it('does not fail if all accounts exist', () => {
Expand Down
27 changes: 17 additions & 10 deletions packages/accounts/src/decode-account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { Decoder } from '@solana/codecs-core';
import {
SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT,
SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT,
SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED,
SolanaError,
} from '@solana/errors';

import type { Account, EncodedAccount } from './account';
import type { MaybeAccount, MaybeEncodedAccount } from './maybe-account';
Expand All @@ -21,11 +27,10 @@ export function decodeAccount<TData extends object, TAddress extends string = st
return encodedAccount;
}
return Object.freeze({ ...encodedAccount, data: decoder.decode(encodedAccount.data) });
} catch (error) {
// TODO: Coded error.
const newError = new Error(`Failed to decode account [${encodedAccount.address}].`);
newError.cause = error;
throw newError;
} catch (e) {
throw new SolanaError(SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT, {
address: encodedAccount.address,
});
}
}

Expand All @@ -44,8 +49,9 @@ export function assertAccountDecoded<TData extends object, TAddress extends stri
account: Account<TData | Uint8Array, TAddress> | MaybeAccount<TData | Uint8Array, TAddress>,
): asserts account is Account<TData, TAddress> | MaybeAccount<TData, TAddress> {
if (accountExists(account) && account.data instanceof Uint8Array) {
// TODO: coded error.
throw new Error(`Expected account [${account.address}] to be decoded.`);
throw new SolanaError(SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT, {
address: account.address,
});
}
}

Expand All @@ -61,8 +67,9 @@ export function assertAccountsDecoded<TData extends object, TAddress extends str
): asserts accounts is (Account<TData, TAddress> | MaybeAccount<TData, TAddress>)[] {
const encoded = accounts.filter(a => accountExists(a) && a.data instanceof Uint8Array);
if (encoded.length > 0) {
const encodedAddresses = encoded.map(a => a.address).join(', ');
// TODO: Coded error.
throw new Error(`Expected accounts [${encodedAddresses}] to be decoded.`);
const encodedAddresses = encoded.map(a => a.address);
throw new SolanaError(SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED, {
addresses: encodedAddresses,
});
}
}
11 changes: 7 additions & 4 deletions packages/accounts/src/maybe-account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Address } from '@solana/addresses';
import {
SOLANA_ERROR__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND,
SolanaError,
} from '@solana/errors';

import { Account } from './account';

Expand All @@ -15,8 +20,7 @@ export function assertAccountExists<TData extends object | Uint8Array, TAddress
account: MaybeAccount<TData, TAddress>,
): asserts account is Account<TData, TAddress> & { exists: true } {
if (!account.exists) {
// TODO: Coded error.
throw new Error(`Expected account [${account.address}] to exist.`);
throw new SolanaError(SOLANA_ERROR__ACCOUNT_NOT_FOUND, { address: account.address });
}
}

Expand All @@ -27,7 +31,6 @@ export function assertAccountsExist<TData extends object | Uint8Array, TAddress
const missingAccounts = accounts.filter(a => !a.exists);
if (missingAccounts.length > 0) {
const missingAddresses = missingAccounts.map(a => a.address);
// TODO: Coded error.
throw new Error(`Expected accounts [${missingAddresses.join(', ')}] to exist.`);
throw new SolanaError(SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND, { addresses: missingAddresses });
}
}
10 changes: 10 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const SOLANA_ERROR__INVALID_KEYPAIR_BYTES = 4 as const;
export const SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED = 5 as const;
export const SOLANA_ERROR__NONCE_INVALID = 6 as const;
export const SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND = 7 as const;
export const SOLANA_ERROR__ACCOUNT_NOT_FOUND = 8 as const;
export const SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND = 9 as const;
export const SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT = 10 as const;
export const SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT = 11 as const;
export const SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED = 12 as const;
// Reserve error codes starting with [4615000-4615999] for the Rust enum `InstructionError`
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN = 4615000 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_GENERIC_ERROR = 4615001 as const;
Expand Down Expand Up @@ -142,6 +147,11 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED
| typeof SOLANA_ERROR__NONCE_INVALID
| typeof SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND
| typeof SOLANA_ERROR__ACCOUNT_NOT_FOUND
| typeof SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND
| typeof SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT
| typeof SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT
| typeof SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_GENERIC_ERROR
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ARGUMENT
Expand Down
20 changes: 20 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {
SOLANA_ERROR__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED,
SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT,
SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT,
SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_ALREADY_INITIALIZED,
SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_FAILED,
SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_OUTSTANDING,
Expand Down Expand Up @@ -56,8 +59,10 @@ import {
SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_PROGRAM_ID,
SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR,
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND,
SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND,
SOLANA_ERROR__NONCE_INVALID,
SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED,
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION,
SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT,
Expand Down Expand Up @@ -144,10 +149,19 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_INSTRUCTION_TRACE_LENGTH_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_BUILTIN_PROGRAMS_MUST_CONSUME_COMPUTE_UNITS
> & {
[SOLANA_ERROR__ACCOUNT_NOT_FOUND]: {
address: string;
};
[SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED]: {
currentBlockHeight: bigint;
lastValidBlockHeight: bigint;
};
[SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT]: {
address: string;
};
[SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT]: {
address: string;
};
[SOLANA_ERROR__INSTRUCTION_ERROR_BORSH_IO_ERROR]: {
encodedData: string;
index: number;
Expand All @@ -164,13 +178,19 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: {
byteLength: number;
};
[SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND]: {
addresses: string[];
};
[SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND]: {
nonceAccountAddress: string;
};
[SOLANA_ERROR__NONCE_INVALID]: {
actualNonceValue: string;
expectedNonceValue: string;
};
[SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED]: {
addresses: string[];
};
[SOLANA_ERROR__RPC_INTEGER_OVERFLOW]: {
argumentLabel: string;
keyPath: readonly (string | number | symbol)[];
Expand Down
11 changes: 11 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {
SOLANA_ERROR__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED,
SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT,
SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT,
SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_ALREADY_INITIALIZED,
SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_FAILED,
SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_OUTSTANDING,
Expand Down Expand Up @@ -56,8 +59,10 @@ import {
SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_PROGRAM_ID,
SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR,
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND,
SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND,
SOLANA_ERROR__NONCE_INVALID,
SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED,
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_BORROW_OUTSTANDING,
SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_IN_USE,
Expand Down Expand Up @@ -122,8 +127,11 @@ export const SolanaErrorMessages: Readonly<{
// TypeScript will fail to build this project if add an error code without a message.
[P in SolanaErrorCode]: string;
}> = {
[SOLANA_ERROR__ACCOUNT_NOT_FOUND]: 'Account not found at address: $address',
[SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED]:
'The network has progressed past the last block for which this transaction could have been committed.',
[SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT]: 'Expected decoded account at address: $address',
[SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT]: 'Failed to decode account data at address: $address',
[SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_ALREADY_INITIALIZED]: 'instruction requires an uninitialized account',
[SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_FAILED]:
'instruction tries to borrow reference for an account which is already borrowed',
Expand Down Expand Up @@ -195,9 +203,12 @@ export const SolanaErrorMessages: Readonly<{
[SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_PROGRAM_ID]: 'Unsupported program id',
[SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR]: 'Unsupported sysvar',
[SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: 'Key pair bytes must be of length 64, got $byteLength.',
[SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND]: 'Accounts not found at addresses: $addresses',
[SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND]: 'No nonce account could be found at address `$nonceAccountAddress`',
[SOLANA_ERROR__NONCE_INVALID]:
'The nonce `$expectedNonceValue` is no longer valid. It has advanced to `$actualNonceValue`',
[SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED]:
'Not all accounts were decoded. Encoded accounts found at addresses: $addresses.',
[SOLANA_ERROR__RPC_INTEGER_OVERFLOW]:
'The $argumentLabel argument to the `$methodName` RPC method$optionalPathLabel was ' +
'`$value`. This number is unsafe for use with the Solana JSON-RPC because it exceeds ' +
Expand Down
3 changes: 3 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 546263e

Please sign in to comment.