Skip to content

Commit

Permalink
Convert errors to coded exceptions in @solana/keys
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Mar 4, 2024
1 parent 097c92d commit 4d0a5cd
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 26 deletions.
7 changes: 7 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export const SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_N
3507001 as const;
export const SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING = 3507002 as const;
export const SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE = 3507003 as const;
// Reserve key-related error codes in the range [3704000-3704999]
export const SOLANA_ERROR__KEYS_PRIVATE_KEY_BYTE_LENGTH_OUT_OF_RANGE = 3704000 as const;
export const SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE = 3704001 as const;
export const SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE = 3704002 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 @@ -332,6 +336,9 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE
| typeof SOLANA_ERROR__KEYS_PRIVATE_KEY_BYTE_LENGTH_OUT_OF_RANGE
| typeof SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE
| typeof SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_CANNOT_CREATE_SUBSCRIPTION_REQUEST
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_EXPECTED_SERVER_SUBSCRIPTION_ID
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED
Expand Down
12 changes: 12 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ import {
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
SOLANA_ERROR__KEYS_PRIVATE_KEY_BYTE_LENGTH_OUT_OF_RANGE,
SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE,
SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED,
Expand Down Expand Up @@ -283,6 +286,15 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE]: {
unexpectedValue: unknown;
};
[SOLANA_ERROR__KEYS_PRIVATE_KEY_BYTE_LENGTH_OUT_OF_RANGE]: {
actualLength: number;
};
[SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE]: {
actualLength: number;
};
[SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]: {
actualLength: number;
};
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: {
value: string;
};
Expand Down
9 changes: 9 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ import {
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
SOLANA_ERROR__KEYS_PRIVATE_KEY_BYTE_LENGTH_OUT_OF_RANGE,
SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE,
SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE,
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
Expand Down Expand Up @@ -321,6 +324,12 @@ export const SolanaErrorMessages: Readonly<{
[SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING]:
'Invariant violation: WebSocket message iterator is missing state storage. It should be ' +
'impossible to hit this error; please file an issue at https://sola.na/web3invariant',
[SOLANA_ERROR__KEYS_PRIVATE_KEY_BYTE_LENGTH_OUT_OF_RANGE]:
'Expected private key bytes with length 32. Actual length: $actualLength.',
[SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE]:
'Expected base58-encoded signature to decode to a byte array of length 64. Actual length: $actualLength.',
[SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]:
'Expected base58-encoded signature string of length in the range [64, 88]. Actual length: $actualLength.',
[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`',
Expand Down
27 changes: 24 additions & 3 deletions packages/keys/src/__tests__/coercions-test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE,
SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
SolanaError,
} from '@solana/errors';

import { Signature, signature } from '../signatures';

describe('signature', () => {
Expand All @@ -10,8 +16,23 @@ describe('signature', () => {
);
expect(coerced).toBe(raw);
});
it('throws on invalid `Signature`', () => {
const thisThrows = () => signature('test');
expect(thisThrows).toThrow('`test` is not a signature');
it.each([63, 89])('throws on a `Signature` whose string length is %s', actualLength => {
const thisThrows = () => signature('t'.repeat(actualLength));
expect(thisThrows).toThrow(
new SolanaError(SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE, {
actualLength,
}),
);
});
it.each([
[63, '3bwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKpZqCoP'],
[65, 'ZbwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKPZqCoPZZ'],
])('throws on a `Signature` whose decoded byte length is %s', (actualLength, encodedSignature) => {
const thisThrows = () => signature(encodedSignature);
expect(thisThrows).toThrow(
new SolanaError(SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE, {
actualLength,
}),
);
});
});
10 changes: 7 additions & 3 deletions packages/keys/src/private-key.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SOLANA_ERROR__KEYS_PRIVATE_KEY_BYTE_LENGTH_OUT_OF_RANGE, SolanaError } from '@solana/errors';

function addPkcs8Header(bytes: Uint8Array): Uint8Array {
// prettier-ignore
return new Uint8Array([
Expand Down Expand Up @@ -36,9 +38,11 @@ function addPkcs8Header(bytes: Uint8Array): Uint8Array {
}

export async function createPrivateKeyFromBytes(bytes: Uint8Array, extractable?: boolean): Promise<CryptoKey> {
if (bytes.byteLength !== 32) {
// TODO: Coded error.
throw new Error('Private key bytes must be of length 32');
const actualLength = bytes.byteLength;
if (actualLength !== 32) {
throw new SolanaError(SOLANA_ERROR__KEYS_PRIVATE_KEY_BYTE_LENGTH_OUT_OF_RANGE, {
actualLength,
});
}
const privateKeyBytesPkcs8 = addPkcs8Header(bytes);
return await crypto.subtle.importKey('pkcs8', privateKeyBytesPkcs8, 'Ed25519', extractable ?? false, ['sign']);
Expand Down
42 changes: 22 additions & 20 deletions packages/keys/src/signatures.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { assertSigningCapabilityIsAvailable, assertVerificationCapabilityIsAvailable } from '@solana/assertions';
import { Encoder } from '@solana/codecs-core';
import { getBase58Encoder } from '@solana/codecs-strings';
import {
SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE,
SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
SolanaError,
} from '@solana/errors';

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

export function assertIsSignature(putativeSignature: string): asserts putativeSignature is Signature {
if (!base58Encoder) base58Encoder = getBase58Encoder();

try {
// Fast-path; see if the input string is of an acceptable length.
if (
// Lowest value (64 bytes of zeroes)
putativeSignature.length < 64 ||
// Highest value (64 bytes of 255)
putativeSignature.length > 88
) {
throw new Error('Expected input string to decode to a byte array of length 64.');
}
// Slow-path; actually attempt to decode the input string.
const bytes = base58Encoder.encode(putativeSignature);
const numBytes = bytes.byteLength;
if (numBytes !== 64) {
throw new Error(`Expected input string to decode to a byte array of length 64. Actual length: ${numBytes}`);
}
} catch (e) {
throw new Error(`\`${putativeSignature}\` is not a signature`, {
cause: e,
// Fast-path; see if the input string is of an acceptable length.
if (
// Lowest value (64 bytes of zeroes)
putativeSignature.length < 64 ||
// Highest value (64 bytes of 255)
putativeSignature.length > 88
) {
throw new SolanaError(SOLANA_ERROR__KEYS_SIGNATURE_STRING_LENGTH_OUT_OF_RANGE, {
actualLength: putativeSignature.length,
});
}
// Slow-path; actually attempt to decode the input string.
const bytes = base58Encoder.encode(putativeSignature);
const numBytes = bytes.byteLength;
if (numBytes !== 64) {
throw new SolanaError(SOLANA_ERROR__KEYS_SIGNATURE_BYTE_LENGTH_OUT_OF_RANGE, {
actualLength: numBytes,
});
}
}
Expand Down

0 comments on commit 4d0a5cd

Please sign in to comment.