Skip to content

Commit

Permalink
refactor(experimental): errors: codecs-core package (#2189)
Browse files Browse the repository at this point in the history
Adds custom `SolanaError` throws to the `@solana/codecs-core` package.
  • Loading branch information
buffalojoec committed Feb 29, 2024
1 parent 080a7d1 commit 7712fc3
Show file tree
Hide file tree
Showing 17 changed files with 168 additions and 39 deletions.
3 changes: 3 additions & 0 deletions packages/codecs-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
"engine": {
"node": ">=17.4"
},
"dependencies": {
"@solana/errors": "workspace:*"
},
"devDependencies": {
"@solana/build-scripts": "workspace:*",
"@solana/eslint-config-solana": "^1.0.2",
Expand Down
20 changes: 18 additions & 2 deletions packages/codecs-core/src/__tests__/combine-codec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
SOLANA_ERROR__CODECS_FIXED_SIZE_ENCODER_DECODER_SIZE_MISMATCH,
SOLANA_ERROR__CODECS_VARIABLE_SIZE_ENCODER_DECODER_MAX_SIZE_MISMATCH,
SolanaError,
} from '@solana/errors';

import { createDecoder, createEncoder, FixedSizeCodec, FixedSizeDecoder, FixedSizeEncoder } from '../codec';
import { combineCodec } from '../combine-codec';

Expand Down Expand Up @@ -51,13 +57,23 @@ describe('combineCodec', () => {
createEncoder({ fixedSize: 1, write: jest.fn() }),
createDecoder({ fixedSize: 2, read: jest.fn() }),
),
).toThrow('Encoder and decoder must have the same fixed size, got [1] and [2]');
).toThrow(
new SolanaError(SOLANA_ERROR__CODECS_FIXED_SIZE_ENCODER_DECODER_SIZE_MISMATCH, {
decoderFixedSize: 2,
encoderFixedSize: 1,
}),
);

expect(() =>
combineCodec(
createEncoder({ getSizeFromValue: jest.fn(), maxSize: 1, write: jest.fn() }),
createDecoder({ read: jest.fn() }),
),
).toThrow('Encoder and decoder must have the same max size, got [1] and [undefined]');
).toThrow(
new SolanaError(SOLANA_ERROR__CODECS_VARIABLE_SIZE_ENCODER_DECODER_MAX_SIZE_MISMATCH, {
decoderMaxSize: undefined,
encoderMaxSize: 1,
}),
);
});
});
14 changes: 12 additions & 2 deletions packages/codecs-core/src/__tests__/fix-codec-test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES, SolanaError } from '@solana/errors';

import { createCodec } from '../codec';
import { fixCodec, fixDecoder, fixEncoder } from '../fix-codec';
import { b, getMockCodec } from './__setup__';
Expand Down Expand Up @@ -57,7 +59,11 @@ describe('fixCodec', () => {
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0);

expect(() => fixCodec(mockCodec, 10).decode(b('08050c0c0f'))).toThrow(
'Codec [fixCodec] expected 10 bytes, got 5.',
new SolanaError(SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES, {
bytesLength: 5,
codecDescription: 'fixCodec',
expected: 10,
}),
);
});

Expand Down Expand Up @@ -141,7 +147,11 @@ describe('fixDecoder', () => {
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0);

expect(() => fixDecoder(mockCodec, 10).decode(b('08050c0c0f'))).toThrow(
'Codec [fixCodec] expected 10 bytes, got 5.',
new SolanaError(SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES, {
bytesLength: 5,
codecDescription: 'fixCodec',
expected: 10,
}),
);
});
});
14 changes: 11 additions & 3 deletions packages/codecs-core/src/__tests__/reverse-codec-test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE, SolanaError } from '@solana/errors';

import { createDecoder, createEncoder } from '../codec';
import { fixCodec } from '../fix-codec';
import { reverseCodec, reverseDecoder, reverseEncoder } from '../reverse-codec';
Expand Down Expand Up @@ -28,7 +30,9 @@ describe('reverseCodec', () => {

// Variable-size codec.
// @ts-expect-error Reversed codec should be fixed-size.
expect(() => reverseCodec(base16)).toThrow('Cannot reverse a codec of variable size');
expect(() => reverseCodec(base16)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE),
);
});
});

Expand All @@ -47,7 +51,9 @@ describe('reverseEncoder', () => {
expect(reversedEncoder.encode(42)).toStrictEqual(new Uint8Array([0, 42]));

// @ts-expect-error Reversed encoder should be fixed-size.
expect(() => reverseEncoder(base16)).toThrow('Cannot reverse a codec of variable size');
expect(() => reverseEncoder(base16)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE),
);
});
});

Expand All @@ -63,6 +69,8 @@ describe('reverseDecoder', () => {
expect(reversedDecoder.read(new Uint8Array([42, 0]), 0)).toStrictEqual(['0-42', 2]);

// @ts-expect-error Reversed decoder should be fixed-size.
expect(() => reverseDecoder(base16)).toThrow('Cannot reverse a codec of variable size');
expect(() => reverseDecoder(base16)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE),
);
});
});
18 changes: 14 additions & 4 deletions packages/codecs-core/src/assertions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import {
SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY,
SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES,
SolanaError,
} from '@solana/errors';

/**
* Asserts that a given byte array is not empty.
*/
export function assertByteArrayIsNotEmptyForCodec(codecDescription: string, bytes: Uint8Array, offset = 0) {
if (bytes.length - offset <= 0) {
// TODO: Coded error.
throw new Error(`Codec [${codecDescription}] cannot decode empty byte arrays.`);
throw new SolanaError(SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY, {
codecDescription,
});
}
}

Expand All @@ -19,7 +26,10 @@ export function assertByteArrayHasEnoughBytesForCodec(
) {
const bytesLength = bytes.length - offset;
if (bytesLength < expected) {
// TODO: Coded error.
throw new Error(`Codec [${codecDescription}] expected ${expected} bytes, got ${bytesLength}.`);
throw new SolanaError(SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES, {
bytesLength,
codecDescription,
expected,
});
}
}
12 changes: 8 additions & 4 deletions packages/codecs-core/src/codec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
SOLANA_ERROR__CODECS_EXPECTED_FIXED_LENGTH_GOT_VARIABLE_LENGTH,
SOLANA_ERROR__CODECS_EXPECTED_VARIABLE_LENGTH_GOT_FIXED_LENGTH,
SolanaError,
} from '@solana/errors';

/**
* Defines an offset in bytes.
*/
Expand Down Expand Up @@ -182,8 +188,7 @@ export function assertIsFixedSize(
codec: { fixedSize: number } | { maxSize?: number },
): asserts codec is { fixedSize: number } {
if (!isFixedSize(codec)) {
// TODO: Coded error.
throw new Error('Expected a fixed-size codec, got a variable-size one.');
throw new SolanaError(SOLANA_ERROR__CODECS_EXPECTED_FIXED_LENGTH_GOT_VARIABLE_LENGTH);
}
}

Expand All @@ -209,7 +214,6 @@ export function assertIsVariableSize(
codec: { fixedSize: number } | { maxSize?: number },
): asserts codec is { maxSize?: number } {
if (!isVariableSize(codec)) {
// TODO: Coded error.
throw new Error('Expected a variable-size codec, got a fixed-size one.');
throw new SolanaError(SOLANA_ERROR__CODECS_EXPECTED_VARIABLE_LENGTH_GOT_FIXED_LENGTH);
}
}
26 changes: 16 additions & 10 deletions packages/codecs-core/src/combine-codec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
SOLANA_ERROR__CODECS_ENCODER_DECODER_SIZE_COMPATIBILITY_MISMATCH,
SOLANA_ERROR__CODECS_FIXED_SIZE_ENCODER_DECODER_SIZE_MISMATCH,
SOLANA_ERROR__CODECS_VARIABLE_SIZE_ENCODER_DECODER_MAX_SIZE_MISMATCH,
SolanaError,
} from '@solana/errors';

import {
Codec,
Decoder,
Expand Down Expand Up @@ -33,22 +40,21 @@ export function combineCodec<TFrom, TTo extends TFrom>(
decoder: Decoder<TTo>,
): Codec<TFrom, TTo> {
if (isFixedSize(encoder) !== isFixedSize(decoder)) {
// TODO: Coded error.
throw new Error(`Encoder and decoder must either both be fixed-size or variable-size.`);
throw new SolanaError(SOLANA_ERROR__CODECS_ENCODER_DECODER_SIZE_COMPATIBILITY_MISMATCH);
}

if (isFixedSize(encoder) && isFixedSize(decoder) && encoder.fixedSize !== decoder.fixedSize) {
// TODO: Coded error.
throw new Error(
`Encoder and decoder must have the same fixed size, got [${encoder.fixedSize}] and [${decoder.fixedSize}].`,
);
throw new SolanaError(SOLANA_ERROR__CODECS_FIXED_SIZE_ENCODER_DECODER_SIZE_MISMATCH, {
decoderFixedSize: decoder.fixedSize,
encoderFixedSize: encoder.fixedSize,
});
}

if (!isFixedSize(encoder) && !isFixedSize(decoder) && encoder.maxSize !== decoder.maxSize) {
// TODO: Coded error.
throw new Error(
`Encoder and decoder must have the same max size, got [${encoder.maxSize}] and [${decoder.maxSize}].`,
);
throw new SolanaError(SOLANA_ERROR__CODECS_VARIABLE_SIZE_ENCODER_DECODER_MAX_SIZE_MISMATCH, {
decoderMaxSize: decoder.maxSize,
encoderMaxSize: encoder.maxSize,
});
}

return {
Expand Down
21 changes: 17 additions & 4 deletions packages/codecs-core/src/reverse-codec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
isSolanaError,
SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE,
SOLANA_ERROR__CODECS_EXPECTED_FIXED_LENGTH_GOT_VARIABLE_LENGTH,
SolanaError,
} from '@solana/errors';

import {
assertIsFixedSize,
createDecoder,
Expand All @@ -17,8 +24,11 @@ export function reverseEncoder<TFrom, TSize extends number>(
try {
assertIsFixedSize(encoder);
} catch (e) {
// TODO: Coded error, also proper catch handling
throw new Error('Cannot reverse a codec of variable size.');
if (isSolanaError(e, SOLANA_ERROR__CODECS_EXPECTED_FIXED_LENGTH_GOT_VARIABLE_LENGTH)) {
throw new SolanaError(SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE);
} else {
throw e;
}
}
return createEncoder({
...encoder,
Expand All @@ -40,8 +50,11 @@ export function reverseDecoder<TTo, TSize extends number>(
try {
assertIsFixedSize(decoder);
} catch (e) {
// TODO: Coded error, also proper catch handling
throw new Error('Cannot reverse a codec of variable size.');
if (isSolanaError(e, SOLANA_ERROR__CODECS_EXPECTED_FIXED_LENGTH_GOT_VARIABLE_LENGTH)) {
throw new SolanaError(SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE);
} else {
throw e;
}
}
return createDecoder({
...decoder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('getBitArrayCodec', () => {
expect(bitArray(1).read(b('00'), 0)).toStrictEqual([a('00000000'), 1]);

// It fails if the byte array is too short.
expect(() => bitArray(3).read(b('ff'), 0)).toThrow('Codec [bitArray] expected 3 bytes, got 1');
expect(() => bitArray(3).read(b('ff'), 0)).toThrow(); // `SolanaError` added in later commit
});

it('has the right sizes', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/codecs-data-structures/src/__tests__/bytes-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('getBytesCodec', () => {
expect(bytesU8.read(b('ff022a03ffff'), 1)).toStrictEqual([new Uint8Array([42, 3]), 4]);

// Not enough bytes.
expect(() => bytesU8.read(b('022a'), 0)).toThrow('Codec [bytes] expected 2 bytes, got 1.');
expect(() => bytesU8.read(b('022a'), 0)).toThrow(); // `SolanaError` added in later commit
});

it('encodes fixed bytes', () => {
Expand All @@ -31,7 +31,7 @@ describe('getBytesCodec', () => {
expect(bytes5.encode(new Uint8Array([1, 2]))).toStrictEqual(b('0102000000'));
expect(bytes5.read(b('0102000000'), 0)).toStrictEqual([new Uint8Array([1, 2, 0, 0, 0]), 5]);
expect(bytes5.read(b('ff0102000000'), 1)).toStrictEqual([new Uint8Array([1, 2, 0, 0, 0]), 6]);
expect(() => bytes5.read(b('0102'), 0)).toThrow('Codec [fixCodec] expected 5 bytes, got 2.');
expect(() => bytes5.read(b('0102'), 0)).toThrow(); // `SolanaError` added in later commit

// Too large (truncated).
expect(bytes2.encode(new Uint8Array([1, 2, 3, 4, 5]))).toStrictEqual(b('0102'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ describe('getNullableCodec', () => {

// Fixed nullables must wrap fixed-size items.
// @ts-expect-error It cannot wrap a variable size item when fixed is true.
expect(() => nullable(string(), { fixed: true })).toThrow(
'Fixed nullables can only be used with fixed-size codecs',
);
expect(() => nullable(string(), { fixed: true })).toThrow(); // `SolanaError` added in later commit
});

it('has the right sizes', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/codecs-strings/src/__tests__/string-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('getStringCodec', () => {
expect(string({ size: u8() }).decode(b('03414243'))).toBe('ABC');

// Not enough bytes.
expect(() => string({ size: u8() }).decode(b('0341'))).toThrow('Codec [string] expected 3 bytes, got 1.');
expect(() => string({ size: u8() }).decode(b('0341'))).toThrow(); // `SolanaError` added in later commit
});

it('encodes fixed strings', () => {
Expand Down
16 changes: 16 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export const SOLANA_ERROR__SUBTLE_CRYPTO_EXPORT_FUNCTION_MISSING = 27 as const;
export const SOLANA_ERROR__SUBTLE_CRYPTO_GENERATE_FUNCTION_MISSING = 28 as const;
export const SOLANA_ERROR__SUBTLE_CRYPTO_SIGN_FUNCTION_MISSING = 29 as const;
export const SOLANA_ERROR__SUBTLE_CRYPTO_VERIFY_FUNCTION_MISSING = 30 as const;
export const SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY = 31 as const;
export const SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES = 32 as const;
export const SOLANA_ERROR__CODECS_EXPECTED_FIXED_LENGTH_GOT_VARIABLE_LENGTH = 33 as const;
export const SOLANA_ERROR__CODECS_EXPECTED_VARIABLE_LENGTH_GOT_FIXED_LENGTH = 34 as const;
export const SOLANA_ERROR__CODECS_ENCODER_DECODER_SIZE_COMPATIBILITY_MISMATCH = 35 as const;
export const SOLANA_ERROR__CODECS_FIXED_SIZE_ENCODER_DECODER_SIZE_MISMATCH = 36 as const;
export const SOLANA_ERROR__CODECS_VARIABLE_SIZE_ENCODER_DECODER_MAX_SIZE_MISMATCH = 37 as const;
export const SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE = 38 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 @@ -188,6 +196,14 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__SUBTLE_CRYPTO_GENERATE_FUNCTION_MISSING
| typeof SOLANA_ERROR__SUBTLE_CRYPTO_SIGN_FUNCTION_MISSING
| typeof SOLANA_ERROR__SUBTLE_CRYPTO_VERIFY_FUNCTION_MISSING
| typeof SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY
| typeof SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES
| typeof SOLANA_ERROR__CODECS_EXPECTED_FIXED_LENGTH_GOT_VARIABLE_LENGTH
| typeof SOLANA_ERROR__CODECS_EXPECTED_VARIABLE_LENGTH_GOT_FIXED_LENGTH
| typeof SOLANA_ERROR__CODECS_ENCODER_DECODER_SIZE_COMPATIBILITY_MISMATCH
| typeof SOLANA_ERROR__CODECS_FIXED_SIZE_ENCODER_DECODER_SIZE_MISMATCH
| typeof SOLANA_ERROR__CODECS_VARIABLE_SIZE_ENCODER_DECODER_MAX_SIZE_MISMATCH
| typeof SOLANA_ERROR__CODECS_CANNOT_REVERSE_CODEC_OF_VARIABLE_SIZE
| 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,6 +1,10 @@
import {
SOLANA_ERROR__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED,
SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY,
SOLANA_ERROR__CODECS_FIXED_SIZE_ENCODER_DECODER_SIZE_MISMATCH,
SOLANA_ERROR__CODECS_VARIABLE_SIZE_ENCODER_DECODER_MAX_SIZE_MISMATCH,
SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES,
SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT,
SOLANA_ERROR__FAILED_TO_DECODE_ACCOUNT,
SOLANA_ERROR__INCORRECT_BASE58_ADDRESS_BYTE_LENGTH,
Expand Down Expand Up @@ -162,6 +166,22 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
currentBlockHeight: bigint;
lastValidBlockHeight: bigint;
};
[SOLANA_ERROR__CODECS_CANNOT_DECODE_EMPTY_BYTE_ARRAY]: {
codecDescription: string;
};
[SOLANA_ERROR__CODECS_FIXED_SIZE_ENCODER_DECODER_SIZE_MISMATCH]: {
decoderFixedSize: number;
encoderFixedSize: number;
};
[SOLANA_ERROR__CODECS_VARIABLE_SIZE_ENCODER_DECODER_MAX_SIZE_MISMATCH]: {
decoderMaxSize: number | undefined;
encoderMaxSize: number | undefined;
};
[SOLANA_ERROR__CODECS_WRONG_NUMBER_OF_BYTES]: {
bytesLength: number;
codecDescription: string;
expected: number;
};
[SOLANA_ERROR__EXPECTED_DECODED_ACCOUNT]: {
address: string;
};
Expand Down
Loading

0 comments on commit 7712fc3

Please sign in to comment.