Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Commit a62c388

Browse files
steveluscherpull[bot]
authored andcommitted
Convert errors to coded exceptions in @solana/keys (#2245)
Addresses #2118.
1 parent 03cfae6 commit a62c388

File tree

6 files changed

+81
-26
lines changed

6 files changed

+81
-26
lines changed

packages/errors/src/codes.ts

+7
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export const SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_N
5151
3507001 as const;
5252
export const SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING = 3507002 as const;
5353
export const SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE = 3507003 as const;
54+
// Reserve key-related error codes in the range [3704000-3704999]
55+
export const SOLANA_ERROR__KEYS_INVALID_PRIVATE_KEY_BYTE_LENGTH = 3704000 as const;
56+
export const SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH = 3704001 as const;
57+
export const SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE = 3704002 as const;
5458
// Reserve error codes starting with [4615000-4615999] for the Rust enum `InstructionError`
5559
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN = 4615000 as const;
5660
export const SOLANA_ERROR__INSTRUCTION_ERROR_GENERIC_ERROR = 4615001 as const;
@@ -332,6 +336,9 @@ export type SolanaErrorCode =
332336
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
333337
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING
334338
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE
339+
| typeof SOLANA_ERROR__KEYS_INVALID_PRIVATE_KEY_BYTE_LENGTH
340+
| typeof SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH
341+
| typeof SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE
335342
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_CANNOT_CREATE_SUBSCRIPTION_REQUEST
336343
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_EXPECTED_SERVER_SUBSCRIPTION_ID
337344
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED

packages/errors/src/context.ts

+12
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ import {
7878
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
7979
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
8080
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
81+
SOLANA_ERROR__KEYS_INVALID_PRIVATE_KEY_BYTE_LENGTH,
82+
SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH,
83+
SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
8184
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
8285
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
8386
SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED,
@@ -283,6 +286,15 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
283286
[SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE]: {
284287
unexpectedValue: unknown;
285288
};
289+
[SOLANA_ERROR__KEYS_INVALID_PRIVATE_KEY_BYTE_LENGTH]: {
290+
actualLength: number;
291+
};
292+
[SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH]: {
293+
actualLength: number;
294+
};
295+
[SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]: {
296+
actualLength: number;
297+
};
286298
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: {
287299
value: string;
288300
};

packages/errors/src/messages.ts

+9
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ import {
8585
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
8686
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
8787
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
88+
SOLANA_ERROR__KEYS_INVALID_PRIVATE_KEY_BYTE_LENGTH,
89+
SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH,
90+
SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
8891
SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE,
8992
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
9093
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
@@ -321,6 +324,12 @@ export const SolanaErrorMessages: Readonly<{
321324
[SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING]:
322325
'Invariant violation: WebSocket message iterator is missing state storage. It should be ' +
323326
'impossible to hit this error; please file an issue at https://sola.na/web3invariant',
327+
[SOLANA_ERROR__KEYS_INVALID_PRIVATE_KEY_BYTE_LENGTH]:
328+
'Expected private key bytes with length 32. Actual length: $actualLength.',
329+
[SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH]:
330+
'Expected base58-encoded signature to decode to a byte array of length 64. Actual length: $actualLength.',
331+
[SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]:
332+
'Expected base58-encoded signature string of length in the range [64, 88]. Actual length: $actualLength.',
324333
[SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]',
325334
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`',
326335
[SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`',

packages/keys/src/__tests__/coercions-test.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import {
2+
SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH,
3+
SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
4+
SolanaError,
5+
} from '@solana/errors';
6+
17
import { Signature, signature } from '../signatures';
28

39
describe('signature', () => {
@@ -10,8 +16,23 @@ describe('signature', () => {
1016
);
1117
expect(coerced).toBe(raw);
1218
});
13-
it('throws on invalid `Signature`', () => {
14-
const thisThrows = () => signature('test');
15-
expect(thisThrows).toThrow('`test` is not a signature');
19+
it.each([63, 89])('throws on a `Signature` whose string length is %s', actualLength => {
20+
const thisThrows = () => signature('t'.repeat(actualLength));
21+
expect(thisThrows).toThrow(
22+
new SolanaError(SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE, {
23+
actualLength,
24+
}),
25+
);
26+
});
27+
it.each([
28+
[63, '3bwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKpZqCoP'],
29+
[65, 'ZbwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKPZqCoPZZ'],
30+
])('throws on a `Signature` whose decoded byte length is %s', (actualLength, encodedSignature) => {
31+
const thisThrows = () => signature(encodedSignature);
32+
expect(thisThrows).toThrow(
33+
new SolanaError(SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH, {
34+
actualLength,
35+
}),
36+
);
1637
});
1738
});

packages/keys/src/private-key.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { SOLANA_ERROR__KEYS_INVALID_PRIVATE_KEY_BYTE_LENGTH, SolanaError } from '@solana/errors';
2+
13
function addPkcs8Header(bytes: Uint8Array): Uint8Array {
24
// prettier-ignore
35
return new Uint8Array([
@@ -36,9 +38,11 @@ function addPkcs8Header(bytes: Uint8Array): Uint8Array {
3638
}
3739

3840
export async function createPrivateKeyFromBytes(bytes: Uint8Array, extractable?: boolean): Promise<CryptoKey> {
39-
if (bytes.byteLength !== 32) {
40-
// TODO: Coded error.
41-
throw new Error('Private key bytes must be of length 32');
41+
const actualLength = bytes.byteLength;
42+
if (actualLength !== 32) {
43+
throw new SolanaError(SOLANA_ERROR__KEYS_INVALID_PRIVATE_KEY_BYTE_LENGTH, {
44+
actualLength,
45+
});
4246
}
4347
const privateKeyBytesPkcs8 = addPkcs8Header(bytes);
4448
return await crypto.subtle.importKey('pkcs8', privateKeyBytesPkcs8, 'Ed25519', extractable ?? false, ['sign']);

packages/keys/src/signatures.ts

+22-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { assertSigningCapabilityIsAvailable, assertVerificationCapabilityIsAvailable } from '@solana/assertions';
22
import { Encoder } from '@solana/codecs-core';
33
import { getBase58Encoder } from '@solana/codecs-strings';
4+
import {
5+
SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH,
6+
SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
7+
SolanaError,
8+
} from '@solana/errors';
49

510
export type Signature = string & { readonly __brand: unique symbol };
611
export type SignatureBytes = Uint8Array & { readonly __brand: unique symbol };
@@ -9,26 +14,23 @@ let base58Encoder: Encoder<string> | undefined;
914

1015
export function assertIsSignature(putativeSignature: string): asserts putativeSignature is Signature {
1116
if (!base58Encoder) base58Encoder = getBase58Encoder();
12-
13-
try {
14-
// Fast-path; see if the input string is of an acceptable length.
15-
if (
16-
// Lowest value (64 bytes of zeroes)
17-
putativeSignature.length < 64 ||
18-
// Highest value (64 bytes of 255)
19-
putativeSignature.length > 88
20-
) {
21-
throw new Error('Expected input string to decode to a byte array of length 64.');
22-
}
23-
// Slow-path; actually attempt to decode the input string.
24-
const bytes = base58Encoder.encode(putativeSignature);
25-
const numBytes = bytes.byteLength;
26-
if (numBytes !== 64) {
27-
throw new Error(`Expected input string to decode to a byte array of length 64. Actual length: ${numBytes}`);
28-
}
29-
} catch (e) {
30-
throw new Error(`\`${putativeSignature}\` is not a signature`, {
31-
cause: e,
17+
// Fast-path; see if the input string is of an acceptable length.
18+
if (
19+
// Lowest value (64 bytes of zeroes)
20+
putativeSignature.length < 64 ||
21+
// Highest value (64 bytes of 255)
22+
putativeSignature.length > 88
23+
) {
24+
throw new SolanaError(SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE, {
25+
actualLength: putativeSignature.length,
26+
});
27+
}
28+
// Slow-path; actually attempt to decode the input string.
29+
const bytes = base58Encoder.encode(putativeSignature);
30+
const numBytes = bytes.byteLength;
31+
if (numBytes !== 64) {
32+
throw new SolanaError(SOLANA_ERROR__KEYS_INVALID_SIGNATURE_BYTE_LENGTH, {
33+
actualLength: numBytes,
3234
});
3335
}
3436
}

0 commit comments

Comments
 (0)