From 94fee67560faae1f41aeddb2e7c3d0d9078ab851 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Fri, 1 Mar 2024 14:57:28 -0800 Subject: [PATCH] Convert all errors in `@solana/rpc-types` to coded exceptions (#2226) Addresses #2118. --- packages/errors/src/codes.ts | 12 ++++++ packages/errors/src/context.ts | 20 +++++++++ packages/errors/src/messages.ts | 14 ++++++ packages/rpc-types/package.json | 3 +- .../rpc-types/src/__tests__/blockhash-test.ts | 43 +++++++++++++++---- .../rpc-types/src/__tests__/coercions-test.ts | 30 ++++++++++--- .../rpc-types/src/__tests__/lamports-test.ts | 8 ++-- .../src/__tests__/stringified-bigint-test.ts | 26 +++++++++-- .../src/__tests__/stringified-number-test.ts | 14 +++++- packages/rpc-types/src/blockhash.ts | 41 ++++++++++-------- packages/rpc-types/src/lamports.ts | 12 ++---- packages/rpc-types/src/stringified-bigint.ts | 8 ++-- packages/rpc-types/src/stringified-number.ts | 6 ++- packages/rpc-types/src/unix-timestamp.ts | 13 +++--- pnpm-lock.yaml | 3 ++ 15 files changed, 192 insertions(+), 61 deletions(-) diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 208eb8af928..c49864fe5d0 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -51,6 +51,12 @@ export const SOLANA_ERROR__CODECS_FIXED_NULLABLE_WITH_VARIABLE_SIZE_PREFIX = 44 export const SOLANA_ERROR__CODECS_CODEC_REQUIRES_FIXED_SIZE = 45 as const; export const SOLANA_ERROR__CODECS_NUMBER_OUT_OF_RANGE = 46 as const; export const SOLANA_ERROR__CODECS_INVALID_STRING_FOR_BASE = 47 as const; +export const SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE = 48 as const; +export const SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE = 49 as const; +export const SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE = 50 as const; +export const SOLANA_ERROR__MALFORMED_BIGINT_STRING = 51 as const; +export const SOLANA_ERROR__MALFORMED_NUMBER_STRING = 52 as const; +export const SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE = 53 as const; export const SOLANA_ERROR__EXPECTED_INSTRUCTION_TO_HAVE_ACCOUNTS = 70 as const; export const SOLANA_ERROR__EXPECTED_INSTRUCTION_TO_HAVE_DATA = 71 as const; // Reserve error codes starting with [4615000-4615999] for the Rust enum `InstructionError` @@ -216,6 +222,12 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__INVALID_SEEDS_POINT_ON_CURVE | typeof SOLANA_ERROR__COULD_NOT_FIND_VIABLE_PDA_BUMP_SEED | typeof SOLANA_ERROR__PROGRAM_ADDRESS_ENDS_WITH_PDA_MARKER + | typeof SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE + | typeof SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE + | typeof SOLANA_ERROR__MALFORMED_BIGINT_STRING + | typeof SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE + | typeof SOLANA_ERROR__MALFORMED_NUMBER_STRING + | typeof SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE | typeof SOLANA_ERROR__SUBTLE_CRYPTO_MISSING | typeof SOLANA_ERROR__SUBTLE_CRYPTO_DIGEST_MISSING | typeof SOLANA_ERROR__SUBTLE_CRYPTO_ED25519_ALGORITHM_MISSING diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 3383996c81b..17a6c15ff20 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -3,6 +3,8 @@ import { SOLANA_ERROR__ADDRESS_BYTE_LENGTH_OUT_OF_RANGE, SOLANA_ERROR__ADDRESS_STRING_LENGTH_OUT_OF_RANGE, SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED, + SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE, + SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE, SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY, SOLANA_ERROR__CODECS_CODEC_REQUIRES_FIXED_SIZE, SOLANA_ERROR__CODECS_ENUM_DISCRIMINATOR_OUT_OF_RANGE, @@ -74,6 +76,8 @@ import { SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_PROGRAM_ID, SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR, SOLANA_ERROR__INVALID_KEYPAIR_BYTES, + SOLANA_ERROR__MALFORMED_BIGINT_STRING, + SOLANA_ERROR__MALFORMED_NUMBER_STRING, SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED, SOLANA_ERROR__MAX_PDA_SEED_LENGTH_EXCEEDED, SOLANA_ERROR__MULTIPLE_ACCOUNTS_NOT_FOUND, @@ -92,6 +96,7 @@ import { SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_PARTIAL_SIGNER, SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_SENDING_SIGNER, SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_SIGNER, + SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION, SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT, SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED, @@ -187,6 +192,12 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< [SOLANA_ERROR__ADDRESS_STRING_LENGTH_OUT_OF_RANGE]: { actualLength: number; }; + [SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE]: { + actualLength: number; + }; + [SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE]: { + actualLength: number; + }; [SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED]: { currentBlockHeight: bigint; lastValidBlockHeight: bigint; @@ -263,6 +274,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< [SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: { byteLength: number; }; + [SOLANA_ERROR__MALFORMED_BIGINT_STRING]: { + value: string; + }; [SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED]: { actual: number; maxSeeds: number; @@ -288,6 +302,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< [SOLANA_ERROR__NOT_ALL_ACCOUNTS_DECODED]: { addresses: string[]; }; + [SOLANA_ERROR__MALFORMED_NUMBER_STRING]: { + value: string; + }; [SOLANA_ERROR__PROGRAM_DERIVED_ADDRESS_BUMP_SEED_OUT_OF_RANGE]: { bump: number; }; @@ -326,6 +343,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< [SOLANA_ERROR__SIGNER_EXPECTED_TRANSACTION_SIGNER]: { address: string; }; + [SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE]: { + value: number; + }; [SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION]: { index: number; }; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 706be74e5f6..305812e781d 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -3,6 +3,8 @@ import { SOLANA_ERROR__ADDRESS_BYTE_LENGTH_OUT_OF_RANGE, SOLANA_ERROR__ADDRESS_STRING_LENGTH_OUT_OF_RANGE, SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED, + SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE, + SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE, SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY, SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE, SOLANA_ERROR__CODECS_CODEC_REQUIRES_FIXED_SIZE, @@ -82,6 +84,9 @@ import { SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR, SOLANA_ERROR__INVALID_KEYPAIR_BYTES, SOLANA_ERROR__INVALID_SEEDS_POINT_ON_CURVE, + SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE, + SOLANA_ERROR__MALFORMED_BIGINT_STRING, + SOLANA_ERROR__MALFORMED_NUMBER_STRING, SOLANA_ERROR__MALFORMED_PROGRAM_DERIVED_ADDRESS, SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED, SOLANA_ERROR__MAX_PDA_SEED_LENGTH_EXCEEDED, @@ -112,6 +117,7 @@ import { SOLANA_ERROR__SUBTLE_CRYPTO_MISSING, SOLANA_ERROR__SUBTLE_CRYPTO_SIGN_FUNCTION_MISSING, SOLANA_ERROR__SUBTLE_CRYPTO_VERIFY_FUNCTION_MISSING, + SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_BORROW_OUTSTANDING, SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_IN_USE, SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_LOADED_TWICE, @@ -184,6 +190,10 @@ export const SolanaErrorMessages: Readonly<{ 'Expected base58 encoded address to decode to a byte array of length 32. Actual length: $actualLength.', [SOLANA_ERROR__ADDRESS_STRING_LENGTH_OUT_OF_RANGE]: 'Expected base58-encoded address string of length in the range [32, 44]. Actual length: $actualLength.', + [SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE]: + 'Expected base58 encoded blockhash to decode to a byte array of length 32. Actual length: $actualLength.', + [SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE]: + 'Expected base58-encoded blockash string of length in the range [32, 44]. Actual length: $actualLength.', [SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED]: 'The network has progressed past the last block for which this transaction could have been committed.', [SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY]: 'Codec [$codecDescription] cannot decode empty byte arrays.', @@ -292,6 +302,9 @@ export const SolanaErrorMessages: Readonly<{ [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__INVALID_SEEDS_POINT_ON_CURVE]: 'Invalid seeds; point must fall off the Ed25519 curve.', + [SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]', + [SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`', + [SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`', [SOLANA_ERROR__MALFORMED_PROGRAM_DERIVED_ADDRESS]: 'Expected given program derived address to have the following format: [Address, ProgramDerivedAddressBump].', [SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED]: @@ -351,6 +364,7 @@ export const SolanaErrorMessages: Readonly<{ 'here: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts.', [SOLANA_ERROR__SUBTLE_CRYPTO_SIGN_FUNCTION_MISSING]: 'No signing implementation could be found.', [SOLANA_ERROR__SUBTLE_CRYPTO_VERIFY_FUNCTION_MISSING]: 'No key export implementation could be found.', + [SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE]: 'Timestamp value must be in the range [-8.64e15, 8.64e15]. `$value` given', [SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_BORROW_OUTSTANDING]: 'Transaction processing left an account with an outstanding borrowed reference', [SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_IN_USE]: 'Account in use', diff --git a/packages/rpc-types/package.json b/packages/rpc-types/package.json index 7012cf3f903..dc05aa68e3e 100644 --- a/packages/rpc-types/package.json +++ b/packages/rpc-types/package.json @@ -64,7 +64,8 @@ ], "dependencies": { "@solana/addresses": "workspace:*", - "@solana/codecs-strings": "workspace:*" + "@solana/codecs-strings": "workspace:*", + "@solana/errors": "workspace:*" }, "devDependencies": { "@solana/build-scripts": "workspace:*", diff --git a/packages/rpc-types/src/__tests__/blockhash-test.ts b/packages/rpc-types/src/__tests__/blockhash-test.ts index 77f0d94ffcc..b06db6f88e9 100644 --- a/packages/rpc-types/src/__tests__/blockhash-test.ts +++ b/packages/rpc-types/src/__tests__/blockhash-test.ts @@ -1,5 +1,11 @@ import type { VariableSizeEncoder } from '@solana/codecs-core'; import { getBase58Encoder } from '@solana/codecs-strings'; +import { + SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE, + SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE, + SOLANA_ERROR__CODECS_INVALID_STRING_FOR_BASE, + SolanaError, +} from '@solana/errors'; jest.mock('@solana/codecs-strings', () => ({ ...jest.requireActual('@solana/codecs-strings'), @@ -30,17 +36,38 @@ describe('assertIsBlockhash()', () => { }); it('throws when supplied a non-base58 string', () => { + const badBlockhash = 'not-a-base-58-encoded-string-but-nice-try'; expect(() => { - assertIsBlockhash('not-a-base-58-encoded-string'); - }).toThrow(); + assertIsBlockhash(badBlockhash); + }).toThrow( + new SolanaError(SOLANA_ERROR__CODECS_INVALID_STRING_FOR_BASE, { + alphabet: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', + base: 58, + value: badBlockhash, + }), + ); }); - it('throws when the decoded byte array has a length other than 32 bytes', () => { + it.each([31, 45])('throws when the encoded string is of length %s', actualLength => { + const badBlockhash = '1'.repeat(actualLength); expect(() => { - assertIsBlockhash( - // 31 bytes [128, ..., 128] - '2xea9jWJ9eca3dFiefTeSPP85c6qXqunCqL2h2JNffM', - ); - }).toThrow(); + assertIsBlockhash(badBlockhash); + }).toThrow( + new SolanaError(SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE, { + actualLength, + }), + ); + }); + it.each([ + [31, 'tVojvhToWjQ8Xvo4UPx2Xz9eRy7auyYMmZBjc2XfN'], + [33, 'JJEfe6DcPM2ziB2vfUWDV6aHVerXRGkv3TcyvJUNGHZz'], + ])('throws when the decoded byte array has a length of %s bytes', (actualLength, badBlockhash) => { + expect(() => { + assertIsBlockhash(badBlockhash); + }).toThrow( + new SolanaError(SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE, { + actualLength, + }), + ); }); it('does not throw when supplied a base-58 encoded hash', () => { expect(() => { diff --git a/packages/rpc-types/src/__tests__/coercions-test.ts b/packages/rpc-types/src/__tests__/coercions-test.ts index fbaa89dbef3..cdc63879374 100644 --- a/packages/rpc-types/src/__tests__/coercions-test.ts +++ b/packages/rpc-types/src/__tests__/coercions-test.ts @@ -1,3 +1,11 @@ +import { + SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE, + SOLANA_ERROR__MALFORMED_BIGINT_STRING, + SOLANA_ERROR__MALFORMED_NUMBER_STRING, + SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, + SolanaError, +} from '@solana/errors'; + import { lamports, LamportsUnsafeBeyond2Pow53Minus1 } from '../lamports'; import { StringifiedBigInt, stringifiedBigInt } from '../stringified-bigint'; import { StringifiedNumber, stringifiedNumber } from '../stringified-number'; @@ -12,7 +20,7 @@ describe('coercions', () => { }); it('throws on invalid `LamportsUnsafeBeyond2Pow53Minus1`', () => { const thisThrows = () => lamports(-5n); - expect(thisThrows).toThrow('Input for 64-bit unsigned integer cannot be negative'); + expect(thisThrows).toThrow(new SolanaError(SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE)); }); }); describe('stringifiedBigInt', () => { @@ -23,7 +31,11 @@ describe('coercions', () => { }); it('throws on invalid `StringifiedBigInt`', () => { const thisThrows = () => stringifiedBigInt('test'); - expect(thisThrows).toThrow('`test` cannot be parsed as a BigInt'); + expect(thisThrows).toThrow( + new SolanaError(SOLANA_ERROR__MALFORMED_BIGINT_STRING, { + value: 'test', + }), + ); }); }); describe('stringifiedNumber', () => { @@ -34,7 +46,11 @@ describe('coercions', () => { }); it('throws on invalid `StringifiedNumber`', () => { const thisThrows = () => stringifiedNumber('test'); - expect(thisThrows).toThrow('`test` cannot be parsed as a Number'); + expect(thisThrows).toThrow( + new SolanaError(SOLANA_ERROR__MALFORMED_NUMBER_STRING, { + value: 'test', + }), + ); }); }); describe('unixTimestamp', () => { @@ -43,9 +59,13 @@ describe('coercions', () => { const coerced = unixTimestamp(1234); expect(coerced).toBe(raw); }); - it('throws on invalid `UnixTimestamp`', () => { + it('throws on an out-of-range `UnixTimestamp`', () => { const thisThrows = () => unixTimestamp(8.75e15); - expect(thisThrows).toThrow('`8750000000000000` is not a timestamp'); + expect(thisThrows).toThrow( + new SolanaError(SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, { + value: 8.75e15, + }), + ); }); }); }); diff --git a/packages/rpc-types/src/__tests__/lamports-test.ts b/packages/rpc-types/src/__tests__/lamports-test.ts index 8c167a6f20a..65261dacb15 100644 --- a/packages/rpc-types/src/__tests__/lamports-test.ts +++ b/packages/rpc-types/src/__tests__/lamports-test.ts @@ -1,18 +1,20 @@ +import { SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE, SolanaError } from '@solana/errors'; + import { assertIsLamports } from '../lamports'; describe('assertIsLamports()', () => { it('throws when supplied a negative number', () => { expect(() => { assertIsLamports(-1n); - }).toThrow(); + }).toThrow(new SolanaError(SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE)); expect(() => { assertIsLamports(-1000n); - }).toThrow(); + }).toThrow(new SolanaError(SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE)); }); it('throws when supplied a too large number', () => { expect(() => { assertIsLamports(2n ** 64n); - }).toThrow(); + }).toThrow(new SolanaError(SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE)); }); it('does not throw when supplied zero lamports', () => { expect(() => { diff --git a/packages/rpc-types/src/__tests__/stringified-bigint-test.ts b/packages/rpc-types/src/__tests__/stringified-bigint-test.ts index 3c1278ef3da..1e8467aae12 100644 --- a/packages/rpc-types/src/__tests__/stringified-bigint-test.ts +++ b/packages/rpc-types/src/__tests__/stringified-bigint-test.ts @@ -1,21 +1,39 @@ +import { SOLANA_ERROR__MALFORMED_BIGINT_STRING, SolanaError } from '@solana/errors'; + import { assertIsStringifiedBigInt } from '../stringified-bigint'; describe('assertIsStringifiedBigInt()', () => { it("throws when supplied a string that can't parse as a number", () => { expect(() => { assertIsStringifiedBigInt('abc'); - }).toThrow(); + }).toThrow( + new SolanaError(SOLANA_ERROR__MALFORMED_BIGINT_STRING, { + value: 'abc', + }), + ); expect(() => { assertIsStringifiedBigInt('123a'); - }).toThrow(); + }).toThrow( + new SolanaError(SOLANA_ERROR__MALFORMED_BIGINT_STRING, { + value: '123a', + }), + ); }); it("throws when supplied a string that can't parse as an integer", () => { expect(() => { assertIsStringifiedBigInt('123.0'); - }).toThrow(); + }).toThrow( + new SolanaError(SOLANA_ERROR__MALFORMED_BIGINT_STRING, { + value: '123.0', + }), + ); expect(() => { assertIsStringifiedBigInt('123.5'); - }).toThrow(); + }).toThrow( + new SolanaError(SOLANA_ERROR__MALFORMED_BIGINT_STRING, { + value: '123.5', + }), + ); }); it('does not throw when supplied a string that parses as an integer', () => { expect(() => { diff --git a/packages/rpc-types/src/__tests__/stringified-number-test.ts b/packages/rpc-types/src/__tests__/stringified-number-test.ts index dae5f550c37..641c12f4aaa 100644 --- a/packages/rpc-types/src/__tests__/stringified-number-test.ts +++ b/packages/rpc-types/src/__tests__/stringified-number-test.ts @@ -1,13 +1,23 @@ +import { SOLANA_ERROR__MALFORMED_NUMBER_STRING, SolanaError } from '@solana/errors'; + import { assertIsStringifiedNumber } from '../stringified-number'; describe('assertIsStringifiedNumber()', () => { it("throws when supplied a string that can't parse as a number", () => { expect(() => { assertIsStringifiedNumber('abc'); - }).toThrow(); + }).toThrow( + new SolanaError(SOLANA_ERROR__MALFORMED_NUMBER_STRING, { + value: 'abc', + }), + ); expect(() => { assertIsStringifiedNumber('123a'); - }).toThrow(); + }).toThrow( + new SolanaError(SOLANA_ERROR__MALFORMED_NUMBER_STRING, { + value: '123a', + }), + ); }); it('does not throw when supplied a string that parses as a float', () => { expect(() => { diff --git a/packages/rpc-types/src/blockhash.ts b/packages/rpc-types/src/blockhash.ts index 1beecad26a5..092db649abd 100644 --- a/packages/rpc-types/src/blockhash.ts +++ b/packages/rpc-types/src/blockhash.ts @@ -1,5 +1,10 @@ import type { Encoder } from '@solana/codecs-core'; import { getBase58Encoder } from '@solana/codecs-strings'; +import { + SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE, + SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE, + SolanaError, +} from '@solana/errors'; export type Blockhash = string & { readonly __brand: unique symbol }; @@ -7,25 +12,23 @@ let base58Encoder: Encoder | undefined; export function assertIsBlockhash(putativeBlockhash: string): asserts putativeBlockhash is Blockhash { if (!base58Encoder) base58Encoder = getBase58Encoder(); - try { - // Fast-path; see if the input string is of an acceptable length. - if ( - // Lowest value (32 bytes of zeroes) - putativeBlockhash.length < 32 || - // Highest value (32 bytes of 255) - putativeBlockhash.length > 44 - ) { - throw new Error('Expected input string to decode to a byte array of length 32.'); - } - // Slow-path; actually attempt to decode the input string. - const bytes = base58Encoder.encode(putativeBlockhash); - const numBytes = bytes.byteLength; - if (numBytes !== 32) { - throw new Error(`Expected input string to decode to a byte array of length 32. Actual length: ${numBytes}`); - } - } catch (e) { - throw new Error(`\`${putativeBlockhash}\` is not a blockhash`, { - cause: e, + // Fast-path; see if the input string is of an acceptable length. + if ( + // Lowest value (32 bytes of zeroes) + putativeBlockhash.length < 32 || + // Highest value (32 bytes of 255) + putativeBlockhash.length > 44 + ) { + throw new SolanaError(SOLANA_ERROR__BLOCKHASH_STRING_LENGTH_OUT_OF_RANGE, { + actualLength: putativeBlockhash.length, + }); + } + // Slow-path; actually attempt to decode the input string. + const bytes = base58Encoder.encode(putativeBlockhash); + const numBytes = bytes.byteLength; + if (numBytes !== 32) { + throw new SolanaError(SOLANA_ERROR__BLOCKHASH_BYTE_LENGTH_OUT_OF_RANGE, { + actualLength: numBytes, }); } } diff --git a/packages/rpc-types/src/lamports.ts b/packages/rpc-types/src/lamports.ts index 26af7339508..1d5608a450e 100644 --- a/packages/rpc-types/src/lamports.ts +++ b/packages/rpc-types/src/lamports.ts @@ -1,3 +1,5 @@ +import { SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE, SolanaError } from '@solana/errors'; + // FIXME(solana-labs/solana/issues/30341) Beware that any value above 9007199254740991 may be // truncated or rounded because of a downcast to JavaScript `number` between your calling code and // the JSON-RPC transport. @@ -13,14 +15,8 @@ export function isLamports(putativeLamports: bigint): putativeLamports is Lampor export function assertIsLamports( putativeLamports: bigint, ): asserts putativeLamports is LamportsUnsafeBeyond2Pow53Minus1 { - if (putativeLamports < 0) { - // TODO: Coded error. - throw new Error('Input for 64-bit unsigned integer cannot be negative'); - } - - if (putativeLamports > maxU64Value) { - // TODO: Coded error. - throw new Error('Input number is too large to be represented as a 64-bit unsigned integer'); + if (putativeLamports < 0 || putativeLamports > maxU64Value) { + throw new SolanaError(SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE); } } diff --git a/packages/rpc-types/src/stringified-bigint.ts b/packages/rpc-types/src/stringified-bigint.ts index 2e448d85b06..9b8eec07c6f 100644 --- a/packages/rpc-types/src/stringified-bigint.ts +++ b/packages/rpc-types/src/stringified-bigint.ts @@ -1,3 +1,5 @@ +import { SOLANA_ERROR__MALFORMED_BIGINT_STRING, SolanaError } from '@solana/errors'; + export type StringifiedBigInt = string & { readonly __brand: unique symbol }; export function isStringifiedBigInt(putativeBigInt: string): putativeBigInt is StringifiedBigInt { @@ -12,9 +14,9 @@ export function isStringifiedBigInt(putativeBigInt: string): putativeBigInt is S export function assertIsStringifiedBigInt(putativeBigInt: string): asserts putativeBigInt is StringifiedBigInt { try { BigInt(putativeBigInt); - } catch (e) { - throw new Error(`\`${putativeBigInt}\` cannot be parsed as a BigInt`, { - cause: e, + } catch { + throw new SolanaError(SOLANA_ERROR__MALFORMED_BIGINT_STRING, { + value: putativeBigInt, }); } } diff --git a/packages/rpc-types/src/stringified-number.ts b/packages/rpc-types/src/stringified-number.ts index dafb39ed154..6a581ff792b 100644 --- a/packages/rpc-types/src/stringified-number.ts +++ b/packages/rpc-types/src/stringified-number.ts @@ -1,3 +1,5 @@ +import { SOLANA_ERROR__MALFORMED_NUMBER_STRING, SolanaError } from '@solana/errors'; + export type StringifiedNumber = string & { readonly __brand: unique symbol }; export function isStringifiedNumber(putativeNumber: string): putativeNumber is StringifiedNumber { @@ -6,7 +8,9 @@ export function isStringifiedNumber(putativeNumber: string): putativeNumber is S export function assertIsStringifiedNumber(putativeNumber: string): asserts putativeNumber is StringifiedNumber { if (Number.isNaN(Number(putativeNumber))) { - throw new Error(`\`${putativeNumber}\` cannot be parsed as a Number`); + throw new SolanaError(SOLANA_ERROR__MALFORMED_NUMBER_STRING, { + value: putativeNumber, + }); } } diff --git a/packages/rpc-types/src/unix-timestamp.ts b/packages/rpc-types/src/unix-timestamp.ts index dfc9efca7e0..433a936fdbe 100644 --- a/packages/rpc-types/src/unix-timestamp.ts +++ b/packages/rpc-types/src/unix-timestamp.ts @@ -1,3 +1,5 @@ +import { SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, SolanaError } from '@solana/errors'; + export type UnixTimestamp = number & { readonly __brand: unique symbol }; export function isUnixTimestamp(putativeTimestamp: number): putativeTimestamp is UnixTimestamp { @@ -10,13 +12,10 @@ export function isUnixTimestamp(putativeTimestamp: number): putativeTimestamp is export function assertIsUnixTimestamp(putativeTimestamp: number): asserts putativeTimestamp is UnixTimestamp { // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date - try { - if (putativeTimestamp > 8.64e15 || putativeTimestamp < -8.64e15) { - throw new Error('Expected input number to be in the range [-8.64e15, 8.64e15]'); - } - } catch (e) { - throw new Error(`\`${putativeTimestamp}\` is not a timestamp`, { - cause: e, + + if (putativeTimestamp > 8.64e15 || putativeTimestamp < -8.64e15) { + throw new SolanaError(SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, { + value: putativeTimestamp, }); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd634a4e740..610a091bdf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2635,6 +2635,9 @@ importers: '@solana/codecs-strings': specifier: workspace:* version: link:../codecs-strings + '@solana/errors': + specifier: workspace:* + version: link:../errors devDependencies: '@solana/build-scripts': specifier: workspace:*