diff --git a/.changeset/loud-otters-pull.md b/.changeset/loud-otters-pull.md new file mode 100644 index 000000000000..3fdfd03bec0f --- /dev/null +++ b/.changeset/loud-otters-pull.md @@ -0,0 +1,8 @@ +--- +'@solana/codecs-data-structures': patch +'@solana/codecs-core': patch +'@solana/codecs': patch +'@solana/errors': patch +--- + +`getScalarEnumCodec` is now called `getEnumCodec` diff --git a/packages/codecs-core/README.md b/packages/codecs-core/README.md index bf613a29626b..d21141460c8c 100644 --- a/packages/codecs-core/README.md +++ b/packages/codecs-core/README.md @@ -43,7 +43,7 @@ There is a significant library of composable codecs at your disposal, enabling y - [`@solana/codecs-numbers`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-numbers) for number codecs. - [`@solana/codecs-strings`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-strings) for string codecs. -- [`@solana/codecs-data-structures`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures) for many data structure codecs such as objects, arrays, tuples, sets, maps, scalar enums, discriminated unions, booleans, etc. +- [`@solana/codecs-data-structures`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures) for many data structure codecs such as objects, arrays, tuples, sets, maps, enums, discriminated unions, booleans, etc. - [`@solana/options`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/options) for a Rust-like `Option` type and associated codec. You may also be interested in some of the helpers of this `@solana/codecs-core` library such as `mapCodec`, `fixCodec` or `reverseCodec` that create new codecs from existing ones. diff --git a/packages/codecs-data-structures/README.md b/packages/codecs-data-structures/README.md index 22c6bb8bc5c0..253a9c2159ef 100644 --- a/packages/codecs-data-structures/README.md +++ b/packages/codecs-data-structures/README.md @@ -179,9 +179,9 @@ const bytes = personEncoder.encode({ name: 'alice', age: 42 }); const person = personDecoder.decode(bytes); ``` -## Scalar enum codec +## Enum codec -The `getScalarEnumCodec` function accepts a JavaScript enum constructor and returns a codec for encoding and decoding values of that enum. +The `getEnumCodec` function accepts a JavaScript enum constructor and returns a codec for encoding and decoding values of that enum. ```ts enum Direction { @@ -191,11 +191,11 @@ enum Direction { Down, } -const bytes = getScalarEnumCodec(Direction).encode(Direction.Left); -const direction = getScalarEnumCodec(Direction).decode(bytes); +const bytes = getEnumCodec(Direction).encode(Direction.Left); +const direction = getEnumCodec(Direction).decode(bytes); ``` -When encoding a scalar enum, you may pass the value as an enum value, as a number or even as a string by passing the variant’s name. +When encoding an enum, you may pass the value as an enum value, as a number or even as a string by passing the variant’s name. ```ts enum Direction { @@ -205,23 +205,23 @@ enum Direction { Down, } -getScalarEnumCodec(Direction).encode(Direction.Left); // 0x00 -getScalarEnumCodec(Direction).encode(Direction.Right); // 0x01 -getScalarEnumCodec(Direction).encode(0); // 0x00 -getScalarEnumCodec(Direction).encode(1); // 0x01 -getScalarEnumCodec(Direction).encode('Left'); // 0x00 -getScalarEnumCodec(Direction).encode('Right'); // 0x01 +getEnumCodec(Direction).encode(Direction.Left); // 0x00 +getEnumCodec(Direction).encode(Direction.Right); // 0x01 +getEnumCodec(Direction).encode(0); // 0x00 +getEnumCodec(Direction).encode(1); // 0x01 +getEnumCodec(Direction).encode('Left'); // 0x00 +getEnumCodec(Direction).encode('Right'); // 0x01 ``` As you can see, by default, a `u8` number is being used to store the enum value. However, a number codec may be passed as the `size` option to configure that behaviour. ```ts -const u32DirectionCodec = getScalarEnumCodec(Direction, { size: getU32Codec() }); +const u32DirectionCodec = getEnumCodec(Direction, { size: getU32Codec() }); u32DirectionCodec.encode(Direction.Left); // 0x00000000 u32DirectionCodec.encode(Direction.Right); // 0x01000000 ``` -Note that if you provide a string enum — e.g. `enum Direction { Left = 'LEFT' }` — to the `getScalarEnumCodec` function, it will only store the index of the variant. However, the string value may be used to encode that index. +Note that if you provide a string enum — e.g. `enum Direction { Left = 'LEFT' }` — to the `getEnumCodec` function, it will only store the index of the variant. However, the string value may be used to encode that index. ```ts enum Direction { @@ -231,16 +231,16 @@ enum Direction { Down = 'DOWN', } -getScalarEnumCodec(Direction).encode(Direction.Right); // 0x01 -getScalarEnumCodec(Direction).encode('Right' as Direction); // 0x01 -getScalarEnumCodec(Direction).encode('RIGHT'); // 0x01 +getEnumCodec(Direction).encode(Direction.Right); // 0x01 +getEnumCodec(Direction).encode('Right' as Direction); // 0x01 +getEnumCodec(Direction).encode('RIGHT'); // 0x01 ``` -Separate `getScalarEnumEncoder` and `getScalarEnumDecoder` functions are also available. +Separate `getEnumEncoder` and `getEnumDecoder` functions are also available. ```ts -const bytes = getScalarEnumEncoder(Direction).encode(Direction.Left); -const direction = getScalarEnumDecoder(Direction).decode(bytes); +const bytes = getEnumEncoder(Direction).encode(Direction.Left); +const direction = getEnumDecoder(Direction).decode(bytes); ``` ## Discriminated union codec diff --git a/packages/codecs-data-structures/src/__tests__/enum-test.ts b/packages/codecs-data-structures/src/__tests__/enum-test.ts new file mode 100644 index 000000000000..7d1d1adbc046 --- /dev/null +++ b/packages/codecs-data-structures/src/__tests__/enum-test.ts @@ -0,0 +1,159 @@ +import { getU32Codec, getU64Codec } from '@solana/codecs-numbers'; +import { + SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, + SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, + SolanaError, +} from '@solana/errors'; + +import { getEnumCodec } from '../enum'; +import { b } from './__setup__'; + +describe('getEnumCodec', () => { + const u32 = getU32Codec; + const u64 = getU64Codec; + + enum Empty {} + enum Feedback { + BAD, + GOOD, + } + enum Direction { + UP = 'Up', + DOWN = 'Down', + LEFT = 'Left', + RIGHT = 'Right', + } + enum Hybrid { + NUMERIC, + LEXICAL = 'Lexical', + } + + it('encodes enums', () => { + // Bad. + expect(getEnumCodec(Feedback).encode(Feedback.BAD)).toStrictEqual(b('00')); + expect(getEnumCodec(Feedback).encode('BAD')).toStrictEqual(b('00')); + expect(getEnumCodec(Feedback).encode(0)).toStrictEqual(b('00')); + expect(getEnumCodec(Feedback).read(b('00'), 0)).toStrictEqual([Feedback.BAD, 1]); + expect(getEnumCodec(Feedback).read(b('ffff00'), 2)).toStrictEqual([Feedback.BAD, 3]); + + // Good. + expect(getEnumCodec(Feedback).encode(Feedback.GOOD)).toStrictEqual(b('01')); + expect(getEnumCodec(Feedback).encode('GOOD')).toStrictEqual(b('01')); + expect(getEnumCodec(Feedback).encode(1)).toStrictEqual(b('01')); + expect(getEnumCodec(Feedback).read(b('01'), 0)).toStrictEqual([Feedback.GOOD, 1]); + expect(getEnumCodec(Feedback).read(b('ffff01'), 2)).toStrictEqual([Feedback.GOOD, 3]); + + // Custom size. + const u64Feedback = getEnumCodec(Feedback, { size: u64() }); + expect(u64Feedback.encode(Feedback.GOOD)).toStrictEqual(b('0100000000000000')); + expect(u64Feedback.read(b('0100000000000000'), 0)).toStrictEqual([Feedback.GOOD, 8]); + + // Invalid examples. + // @ts-expect-error Invalid enum variant. + expect(() => getEnumCodec(Feedback).encode('Missing')).toThrow( + new SolanaError(SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, { + maxRange: 1, + minRange: 0, + value: 'Missing', + variants: ['BAD', 'GOOD'], + }), + ); + expect(() => getEnumCodec(Feedback).read(new Uint8Array([2]), 0)).toThrow( + new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, { + discriminator: 2, + maxRange: 1, + minRange: 0, + }), + ); + }); + + it('encodes lexical enums', () => { + // Up. + expect(getEnumCodec(Direction).encode(Direction.UP)).toStrictEqual(b('00')); + expect(getEnumCodec(Direction).encode('UP')).toStrictEqual(b('00')); + expect(getEnumCodec(Direction).encode('Up' as Direction)).toStrictEqual(b('00')); + expect(getEnumCodec(Direction).read(b('00'), 0)).toStrictEqual([Direction.UP, 1]); + expect(getEnumCodec(Direction).read(b('ffff00'), 2)).toStrictEqual([Direction.UP, 3]); + + // Down. + expect(getEnumCodec(Direction).encode(Direction.DOWN)).toStrictEqual(b('01')); + expect(getEnumCodec(Direction).encode('DOWN')).toStrictEqual(b('01')); + expect(getEnumCodec(Direction).encode('Down' as Direction)).toStrictEqual(b('01')); + expect(getEnumCodec(Direction).read(b('01'), 0)).toStrictEqual([Direction.DOWN, 1]); + expect(getEnumCodec(Direction).read(b('ffff01'), 2)).toStrictEqual([Direction.DOWN, 3]); + + // Left. + expect(getEnumCodec(Direction).encode(Direction.LEFT)).toStrictEqual(b('02')); + expect(getEnumCodec(Direction).encode('LEFT')).toStrictEqual(b('02')); + expect(getEnumCodec(Direction).encode('Left' as Direction)).toStrictEqual(b('02')); + expect(getEnumCodec(Direction).read(b('02'), 0)).toStrictEqual([Direction.LEFT, 1]); + expect(getEnumCodec(Direction).read(b('ffff02'), 2)).toStrictEqual([Direction.LEFT, 3]); + + // Right. + expect(getEnumCodec(Direction).encode(Direction.RIGHT)).toStrictEqual(b('03')); + expect(getEnumCodec(Direction).encode('RIGHT')).toStrictEqual(b('03')); + expect(getEnumCodec(Direction).encode('Right' as Direction)).toStrictEqual(b('03')); + expect(getEnumCodec(Direction).read(b('03'), 0)).toStrictEqual([Direction.RIGHT, 1]); + expect(getEnumCodec(Direction).read(b('ffff03'), 2)).toStrictEqual([Direction.RIGHT, 3]); + + // Invalid examples. + // @ts-expect-error Invalid enum variant. + expect(() => getEnumCodec(Direction).encode('Diagonal')).toThrow( + new SolanaError(SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, { + maxRange: 3, + minRange: 0, + value: 'Diagonal', + variants: ['UP', 'DOWN', 'LEFT', 'RIGHT', 'Up', 'Down', 'Left', 'Right'], + }), + ); + expect(() => getEnumCodec(Direction).read(new Uint8Array([4]), 0)).toThrow( + new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, { + discriminator: 4, + maxRange: 3, + minRange: 0, + }), + ); + }); + + it('encodes hybrid enums', () => { + // Numeric. + expect(getEnumCodec(Hybrid).encode(Hybrid.NUMERIC)).toStrictEqual(b('00')); + expect(getEnumCodec(Hybrid).encode('NUMERIC')).toStrictEqual(b('00')); + expect(getEnumCodec(Hybrid).encode(0)).toStrictEqual(b('00')); + expect(getEnumCodec(Hybrid).read(b('00'), 0)).toStrictEqual([Hybrid.NUMERIC, 1]); + expect(getEnumCodec(Hybrid).read(b('ffff00'), 2)).toStrictEqual([Hybrid.NUMERIC, 3]); + + // Lexical. + expect(getEnumCodec(Hybrid).encode(Hybrid.LEXICAL)).toStrictEqual(b('01')); + expect(getEnumCodec(Hybrid).encode('LEXICAL')).toStrictEqual(b('01')); + expect(getEnumCodec(Hybrid).encode('Lexical' as Hybrid)).toStrictEqual(b('01')); + expect(getEnumCodec(Hybrid).read(b('01'), 0)).toStrictEqual([Hybrid.LEXICAL, 1]); + expect(getEnumCodec(Hybrid).read(b('ffff01'), 2)).toStrictEqual([Hybrid.LEXICAL, 3]); + + // Invalid examples. + // @ts-expect-error Invalid enum variant. + expect(() => getEnumCodec(Hybrid).encode('Missing')).toThrow( + new SolanaError(SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, { + maxRange: 1, + minRange: 0, + value: 'Missing', + variants: ['NUMERIC', 'LEXICAL', 'Lexical'], + }), + ); + expect(() => getEnumCodec(Hybrid).read(new Uint8Array([2]), 0)).toThrow( + new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, { + discriminator: 2, + maxRange: 1, + minRange: 0, + }), + ); + }); + + it('has the right sizes', () => { + expect(getEnumCodec(Empty).fixedSize).toBe(1); + expect(getEnumCodec(Feedback).fixedSize).toBe(1); + expect(getEnumCodec(Direction).fixedSize).toBe(1); + expect(getEnumCodec(Hybrid).fixedSize).toBe(1); + expect(getEnumCodec(Feedback, { size: u32() }).fixedSize).toBe(4); + }); +}); diff --git a/packages/codecs-data-structures/src/__tests__/scalar-enum-test.ts b/packages/codecs-data-structures/src/__tests__/scalar-enum-test.ts deleted file mode 100644 index 699ffe68e219..000000000000 --- a/packages/codecs-data-structures/src/__tests__/scalar-enum-test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { getU32Codec, getU64Codec } from '@solana/codecs-numbers'; -import { - SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, - SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, - SolanaError, -} from '@solana/errors'; - -import { getScalarEnumCodec } from '../scalar-enum'; -import { b } from './__setup__'; - -describe('getScalarEnumCodec', () => { - const scalarEnum = getScalarEnumCodec; - const u32 = getU32Codec; - const u64 = getU64Codec; - - enum Empty {} - enum Feedback { - BAD, - GOOD, - } - enum Direction { - UP = 'Up', - DOWN = 'Down', - LEFT = 'Left', - RIGHT = 'Right', - } - enum Hybrid { - NUMERIC, - LEXICAL = 'Lexical', - } - - it('encodes scalar enums', () => { - // Bad. - expect(scalarEnum(Feedback).encode(Feedback.BAD)).toStrictEqual(b('00')); - expect(scalarEnum(Feedback).encode('BAD')).toStrictEqual(b('00')); - expect(scalarEnum(Feedback).encode(0)).toStrictEqual(b('00')); - expect(scalarEnum(Feedback).read(b('00'), 0)).toStrictEqual([Feedback.BAD, 1]); - expect(scalarEnum(Feedback).read(b('ffff00'), 2)).toStrictEqual([Feedback.BAD, 3]); - - // Good. - expect(scalarEnum(Feedback).encode(Feedback.GOOD)).toStrictEqual(b('01')); - expect(scalarEnum(Feedback).encode('GOOD')).toStrictEqual(b('01')); - expect(scalarEnum(Feedback).encode(1)).toStrictEqual(b('01')); - expect(scalarEnum(Feedback).read(b('01'), 0)).toStrictEqual([Feedback.GOOD, 1]); - expect(scalarEnum(Feedback).read(b('ffff01'), 2)).toStrictEqual([Feedback.GOOD, 3]); - - // Custom size. - const u64Feedback = scalarEnum(Feedback, { size: u64() }); - expect(u64Feedback.encode(Feedback.GOOD)).toStrictEqual(b('0100000000000000')); - expect(u64Feedback.read(b('0100000000000000'), 0)).toStrictEqual([Feedback.GOOD, 8]); - - // Invalid examples. - // @ts-expect-error Invalid scalar enum variant. - expect(() => scalarEnum(Feedback).encode('Missing')).toThrow( - new SolanaError(SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, { - maxRange: 1, - minRange: 0, - value: 'Missing', - variants: ['BAD', 'GOOD'], - }), - ); - expect(() => scalarEnum(Feedback).read(new Uint8Array([2]), 0)).toThrow( - new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, { - discriminator: 2, - maxRange: 1, - minRange: 0, - }), - ); - }); - - it('encodes lexical scalar enums', () => { - // Up. - expect(scalarEnum(Direction).encode(Direction.UP)).toStrictEqual(b('00')); - expect(scalarEnum(Direction).encode('UP')).toStrictEqual(b('00')); - expect(scalarEnum(Direction).encode('Up' as Direction)).toStrictEqual(b('00')); - expect(scalarEnum(Direction).read(b('00'), 0)).toStrictEqual([Direction.UP, 1]); - expect(scalarEnum(Direction).read(b('ffff00'), 2)).toStrictEqual([Direction.UP, 3]); - - // Down. - expect(scalarEnum(Direction).encode(Direction.DOWN)).toStrictEqual(b('01')); - expect(scalarEnum(Direction).encode('DOWN')).toStrictEqual(b('01')); - expect(scalarEnum(Direction).encode('Down' as Direction)).toStrictEqual(b('01')); - expect(scalarEnum(Direction).read(b('01'), 0)).toStrictEqual([Direction.DOWN, 1]); - expect(scalarEnum(Direction).read(b('ffff01'), 2)).toStrictEqual([Direction.DOWN, 3]); - - // Left. - expect(scalarEnum(Direction).encode(Direction.LEFT)).toStrictEqual(b('02')); - expect(scalarEnum(Direction).encode('LEFT')).toStrictEqual(b('02')); - expect(scalarEnum(Direction).encode('Left' as Direction)).toStrictEqual(b('02')); - expect(scalarEnum(Direction).read(b('02'), 0)).toStrictEqual([Direction.LEFT, 1]); - expect(scalarEnum(Direction).read(b('ffff02'), 2)).toStrictEqual([Direction.LEFT, 3]); - - // Right. - expect(scalarEnum(Direction).encode(Direction.RIGHT)).toStrictEqual(b('03')); - expect(scalarEnum(Direction).encode('RIGHT')).toStrictEqual(b('03')); - expect(scalarEnum(Direction).encode('Right' as Direction)).toStrictEqual(b('03')); - expect(scalarEnum(Direction).read(b('03'), 0)).toStrictEqual([Direction.RIGHT, 1]); - expect(scalarEnum(Direction).read(b('ffff03'), 2)).toStrictEqual([Direction.RIGHT, 3]); - - // Invalid examples. - // @ts-expect-error Invalid scalar enum variant. - expect(() => scalarEnum(Direction).encode('Diagonal')).toThrow( - new SolanaError(SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, { - maxRange: 3, - minRange: 0, - value: 'Diagonal', - variants: ['UP', 'DOWN', 'LEFT', 'RIGHT', 'Up', 'Down', 'Left', 'Right'], - }), - ); - expect(() => scalarEnum(Direction).read(new Uint8Array([4]), 0)).toThrow( - new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, { - discriminator: 4, - maxRange: 3, - minRange: 0, - }), - ); - }); - - it('encodes hybrid scalar enums', () => { - // Numeric. - expect(scalarEnum(Hybrid).encode(Hybrid.NUMERIC)).toStrictEqual(b('00')); - expect(scalarEnum(Hybrid).encode('NUMERIC')).toStrictEqual(b('00')); - expect(scalarEnum(Hybrid).encode(0)).toStrictEqual(b('00')); - expect(scalarEnum(Hybrid).read(b('00'), 0)).toStrictEqual([Hybrid.NUMERIC, 1]); - expect(scalarEnum(Hybrid).read(b('ffff00'), 2)).toStrictEqual([Hybrid.NUMERIC, 3]); - - // Lexical. - expect(scalarEnum(Hybrid).encode(Hybrid.LEXICAL)).toStrictEqual(b('01')); - expect(scalarEnum(Hybrid).encode('LEXICAL')).toStrictEqual(b('01')); - expect(scalarEnum(Hybrid).encode('Lexical' as Hybrid)).toStrictEqual(b('01')); - expect(scalarEnum(Hybrid).read(b('01'), 0)).toStrictEqual([Hybrid.LEXICAL, 1]); - expect(scalarEnum(Hybrid).read(b('ffff01'), 2)).toStrictEqual([Hybrid.LEXICAL, 3]); - - // Invalid examples. - // @ts-expect-error Invalid scalar enum variant. - expect(() => scalarEnum(Hybrid).encode('Missing')).toThrow( - new SolanaError(SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, { - maxRange: 1, - minRange: 0, - value: 'Missing', - variants: ['NUMERIC', 'LEXICAL', 'Lexical'], - }), - ); - expect(() => scalarEnum(Hybrid).read(new Uint8Array([2]), 0)).toThrow( - new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, { - discriminator: 2, - maxRange: 1, - minRange: 0, - }), - ); - }); - - it('has the right sizes', () => { - expect(scalarEnum(Empty).fixedSize).toBe(1); - expect(scalarEnum(Feedback).fixedSize).toBe(1); - expect(scalarEnum(Direction).fixedSize).toBe(1); - expect(scalarEnum(Hybrid).fixedSize).toBe(1); - expect(scalarEnum(Feedback, { size: u32() }).fixedSize).toBe(4); - }); -}); diff --git a/packages/codecs-data-structures/src/__typetests__/enum-typetest.ts b/packages/codecs-data-structures/src/__typetests__/enum-typetest.ts new file mode 100644 index 000000000000..fce306563df6 --- /dev/null +++ b/packages/codecs-data-structures/src/__typetests__/enum-typetest.ts @@ -0,0 +1,54 @@ +import { + FixedSizeCodec, + FixedSizeDecoder, + FixedSizeEncoder, + VariableSizeCodec, + VariableSizeDecoder, + VariableSizeEncoder, +} from '@solana/codecs-core'; +import { getU32Codec, getU32Decoder, getU32Encoder } from '@solana/codecs-numbers'; + +import { getEnumCodec, getEnumDecoder, getEnumEncoder } from '../enum'; + +enum Feedback { + BAD, + GOOD, +} +type FeedbackInput = Feedback | keyof typeof Feedback; + +enum Direction { + UP = 'Up', + DOWN = 'Down', + LEFT = 'Left', + RIGHT = 'Right', +} +type DirectionInput = Direction | keyof typeof Direction; + +{ + // [getEnumEncoder]: It knows if the encoder is fixed size or variable size. + getEnumEncoder(Feedback) satisfies FixedSizeEncoder; + getEnumEncoder(Direction) satisfies FixedSizeEncoder; + getEnumEncoder(Feedback, { size: getU32Encoder() }) satisfies FixedSizeEncoder; + getEnumEncoder(Feedback, { + size: {} as VariableSizeEncoder, + }) satisfies VariableSizeEncoder; +} + +{ + // [getEnumDecoder]: It knows if the decoder is fixed size or variable size. + getEnumDecoder(Feedback) satisfies FixedSizeDecoder; + getEnumDecoder(Direction) satisfies FixedSizeDecoder; + getEnumDecoder(Feedback, { size: getU32Decoder() }) satisfies FixedSizeDecoder; + getEnumDecoder(Feedback, { size: {} as VariableSizeDecoder }) satisfies VariableSizeDecoder; +} + +{ + // [getEnumCodec]: It knows if the codec is fixed size or variable size. + getEnumCodec(Feedback) satisfies FixedSizeCodec; + getEnumCodec(Direction) satisfies FixedSizeCodec; + getEnumCodec(Feedback, { size: getU32Codec() }) satisfies FixedSizeCodec; + getEnumCodec(Feedback, { size: {} as VariableSizeCodec }) satisfies VariableSizeCodec< + FeedbackInput, + Feedback + >; +} diff --git a/packages/codecs-data-structures/src/__typetests__/scalar-enum-typetest.ts b/packages/codecs-data-structures/src/__typetests__/scalar-enum-typetest.ts deleted file mode 100644 index 3e34a834599b..000000000000 --- a/packages/codecs-data-structures/src/__typetests__/scalar-enum-typetest.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - FixedSizeCodec, - FixedSizeDecoder, - FixedSizeEncoder, - VariableSizeCodec, - VariableSizeDecoder, - VariableSizeEncoder, -} from '@solana/codecs-core'; -import { getU32Codec, getU32Decoder, getU32Encoder } from '@solana/codecs-numbers'; - -import { getScalarEnumCodec, getScalarEnumDecoder, getScalarEnumEncoder } from '../scalar-enum'; - -enum Feedback { - BAD, - GOOD, -} -type FeedbackInput = Feedback | keyof typeof Feedback; - -enum Direction { - UP = 'Up', - DOWN = 'Down', - LEFT = 'Left', - RIGHT = 'Right', -} -type DirectionInput = Direction | keyof typeof Direction; - -{ - // [getScalarEnumEncoder]: It knows if the encoder is fixed size or variable size. - getScalarEnumEncoder(Feedback) satisfies FixedSizeEncoder; - getScalarEnumEncoder(Direction) satisfies FixedSizeEncoder; - getScalarEnumEncoder(Feedback, { size: getU32Encoder() }) satisfies FixedSizeEncoder; - getScalarEnumEncoder(Feedback, { - size: {} as VariableSizeEncoder, - }) satisfies VariableSizeEncoder; -} - -{ - // [getScalarEnumDecoder]: It knows if the decoder is fixed size or variable size. - getScalarEnumDecoder(Feedback) satisfies FixedSizeDecoder; - getScalarEnumDecoder(Direction) satisfies FixedSizeDecoder; - getScalarEnumDecoder(Feedback, { size: getU32Decoder() }) satisfies FixedSizeDecoder; - getScalarEnumDecoder(Feedback, { size: {} as VariableSizeDecoder }) satisfies VariableSizeDecoder; -} - -{ - // [getScalarEnumCodec]: It knows if the codec is fixed size or variable size. - getScalarEnumCodec(Feedback) satisfies FixedSizeCodec; - getScalarEnumCodec(Direction) satisfies FixedSizeCodec; - getScalarEnumCodec(Feedback, { size: getU32Codec() }) satisfies FixedSizeCodec; - getScalarEnumCodec(Feedback, { size: {} as VariableSizeCodec }) satisfies VariableSizeCodec< - FeedbackInput, - Feedback - >; -} diff --git a/packages/codecs-data-structures/src/enum.ts b/packages/codecs-data-structures/src/enum.ts new file mode 100644 index 000000000000..5f5ed8b7006d --- /dev/null +++ b/packages/codecs-data-structures/src/enum.ts @@ -0,0 +1,210 @@ +import { + Codec, + combineCodec, + Decoder, + Encoder, + FixedSizeCodec, + FixedSizeDecoder, + FixedSizeEncoder, + mapDecoder, + mapEncoder, + VariableSizeCodec, + VariableSizeDecoder, + VariableSizeEncoder, +} from '@solana/codecs-core'; +import { + FixedSizeNumberCodec, + FixedSizeNumberDecoder, + FixedSizeNumberEncoder, + getU8Decoder, + getU8Encoder, + NumberCodec, + NumberDecoder, + NumberEncoder, +} from '@solana/codecs-numbers'; +import { + SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, + SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, + SolanaError, +} from '@solana/errors'; + +/** + * Defines the "lookup object" of an enum. + * + * @example + * ```ts + * enum Direction { Left, Right }; + * ``` + */ +export type EnumLookupObject = { [key: string]: number | string }; + +/** + * Returns the allowed input for an enum. + * + * @example + * ```ts + * enum Direction { Left, Right }; + * type DirectionInput = GetEnumFrom; // "Left" | "Right" | 0 | 1 + * ``` + */ +type GetEnumFrom = TEnum[keyof TEnum] | keyof TEnum; + +/** + * Returns all the available variants of an enum. + * + * @example + * ```ts + * enum Direction { Left, Right }; + * type DirectionOutput = GetEnumTo; // 0 | 1 + * ``` + */ +type GetEnumTo = TEnum[keyof TEnum]; + +/** Defines the config for enum codecs. */ +export type EnumCodecConfig = { + /** + * The codec to use for the enum discriminator. + * @defaultValue u8 discriminator. + */ + size?: TDiscriminator; +}; + +/** + * Creates an enum encoder. + * + * @param constructor - The constructor of the enum. + * @param config - A set of config for the encoder. + */ +export function getEnumEncoder( + constructor: TEnum, +): FixedSizeEncoder, 1>; +export function getEnumEncoder( + constructor: TEnum, + config: EnumCodecConfig & { size: FixedSizeNumberEncoder }, +): FixedSizeEncoder, TSize>; +export function getEnumEncoder( + constructor: TEnum, + config?: EnumCodecConfig, +): VariableSizeEncoder>; +export function getEnumEncoder( + constructor: TEnum, + config: EnumCodecConfig = {}, +): Encoder> { + const prefix = config.size ?? getU8Encoder(); + const { minRange, maxRange, allStringInputs, enumKeys, enumValues } = getEnumStats(constructor); + return mapEncoder(prefix, (value: GetEnumFrom): number => { + const isInvalidNumber = typeof value === 'number' && (value < minRange || value > maxRange); + const isInvalidString = typeof value === 'string' && !allStringInputs.includes(value); + if (isInvalidNumber || isInvalidString) { + throw new SolanaError(SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, { + maxRange, + minRange, + value, + variants: allStringInputs, + }); + } + if (typeof value === 'number') return value; + const valueIndex = enumValues.indexOf(value as string); + if (valueIndex >= 0) return valueIndex; + return enumKeys.indexOf(value as string); + }); +} + +/** + * Creates an enum decoder. + * + * @param constructor - The constructor of the enum. + * @param config - A set of config for the decoder. + */ +export function getEnumDecoder( + constructor: TEnum, +): FixedSizeDecoder, 1>; +export function getEnumDecoder( + constructor: TEnum, + config: EnumCodecConfig & { size: FixedSizeNumberDecoder }, +): FixedSizeDecoder, TSize>; +export function getEnumDecoder( + constructor: TEnum, + config?: EnumCodecConfig, +): VariableSizeDecoder>; +export function getEnumDecoder( + constructor: TEnum, + config: EnumCodecConfig = {}, +): Decoder> { + const prefix = config.size ?? getU8Decoder(); + const { minRange, maxRange, enumKeys } = getEnumStats(constructor); + return mapDecoder(prefix, (value: bigint | number): GetEnumTo => { + const valueAsNumber = Number(value); + if (valueAsNumber < minRange || valueAsNumber > maxRange) { + throw new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, { + discriminator: valueAsNumber, + maxRange, + minRange, + }); + } + return constructor[enumKeys[valueAsNumber]] as GetEnumTo; + }); +} + +/** + * Creates an enum codec. + * + * @param constructor - The constructor of the enum. + * @param config - A set of config for the codec. + */ +export function getEnumCodec( + constructor: TEnum, +): FixedSizeCodec, GetEnumTo, 1>; +export function getEnumCodec( + constructor: TEnum, + config: EnumCodecConfig & { size: FixedSizeNumberCodec }, +): FixedSizeCodec, GetEnumTo, TSize>; +export function getEnumCodec( + constructor: TEnum, + config?: EnumCodecConfig, +): VariableSizeCodec, GetEnumTo>; +export function getEnumCodec( + constructor: TEnum, + config: EnumCodecConfig = {}, +): Codec, GetEnumTo> { + return combineCodec(getEnumEncoder(constructor, config), getEnumDecoder(constructor, config)); +} + +function getEnumStats( + constructor: TEnum, +): { + allStringInputs: string[]; + enumKeys: string[]; + enumValues: (number | string)[]; + maxRange: number; + minRange: number; +} { + const numericValues = Object.values(constructor).filter(v => typeof v === 'number') as number[]; + const deduplicatedConstructor = Object.fromEntries( + Object.entries(constructor).slice(numericValues.length), + ) as Record; + const enumKeys = Object.keys(deduplicatedConstructor); + const enumValues = Object.values(deduplicatedConstructor); + const minRange = 0; + const maxRange = enumValues.length - 1; + const allStringInputs: string[] = [ + ...new Set([...enumKeys, ...enumValues.filter((v): v is string => typeof v === 'string')]), + ]; + + return { + allStringInputs, + enumKeys, + enumValues, + maxRange, + minRange, + }; +} + +/** @deprecated Use `getEnumEncoder` instead. */ +export const getScalarEnumEncoder = getEnumEncoder; + +/** @deprecated Use `getEnumDecoder` instead. */ +export const getScalarEnumDecoder = getEnumDecoder; + +/** @deprecated Use `getEnumCodec` instead. */ +export const getScalarEnumCodec = getEnumCodec; diff --git a/packages/codecs-data-structures/src/index.ts b/packages/codecs-data-structures/src/index.ts index 22a9155d1c69..5e3ced9c7726 100644 --- a/packages/codecs-data-structures/src/index.ts +++ b/packages/codecs-data-structures/src/index.ts @@ -4,9 +4,9 @@ export * from './bit-array'; export * from './boolean'; export * from './bytes'; export * from './discriminated-union'; +export * from './enum'; export * from './map'; export * from './nullable'; -export * from './scalar-enum'; export * from './set'; export * from './struct'; export * from './tuple'; diff --git a/packages/codecs-data-structures/src/scalar-enum.ts b/packages/codecs-data-structures/src/scalar-enum.ts deleted file mode 100644 index 0c96f1302251..000000000000 --- a/packages/codecs-data-structures/src/scalar-enum.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { - Codec, - combineCodec, - Decoder, - Encoder, - FixedSizeCodec, - FixedSizeDecoder, - FixedSizeEncoder, - mapDecoder, - mapEncoder, - VariableSizeCodec, - VariableSizeDecoder, - VariableSizeEncoder, -} from '@solana/codecs-core'; -import { - FixedSizeNumberCodec, - FixedSizeNumberDecoder, - FixedSizeNumberEncoder, - getU8Decoder, - getU8Encoder, - NumberCodec, - NumberDecoder, - NumberEncoder, -} from '@solana/codecs-numbers'; -import { - SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, - SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, - SolanaError, -} from '@solana/errors'; - -/** - * Defines the "lookup object" of a scalar enum. - * - * @example - * ```ts - * enum Direction { Left, Right }; - * ``` - */ -export type ScalarEnum = { [key: string]: number | string }; - -/** - * Returns the allowed input for a scalar enum. - * - * @example - * ```ts - * enum Direction { Left, Right }; - * type DirectionInput = ScalarEnumFrom; // "Left" | "Right" | 0 | 1 - * ``` - */ -export type ScalarEnumFrom = TEnum[keyof TEnum] | keyof TEnum; - -/** - * Returns all the available variants of a scalar enum. - * - * @example - * ```ts - * enum Direction { Left, Right }; - * type DirectionOutput = ScalarEnumFrom; // 0 | 1 - * ``` - */ -export type ScalarEnumTo = TEnum[keyof TEnum]; - -/** Defines the config for scalar enum codecs. */ -export type ScalarEnumCodecConfig = { - /** - * The codec to use for the enum discriminator. - * @defaultValue u8 discriminator. - */ - size?: TDiscriminator; -}; - -/** - * Creates a scalar enum encoder. - * - * @param constructor - The constructor of the scalar enum. - * @param config - A set of config for the encoder. - */ -export function getScalarEnumEncoder( - constructor: TEnum, -): FixedSizeEncoder, 1>; -export function getScalarEnumEncoder( - constructor: TEnum, - config: ScalarEnumCodecConfig & { size: FixedSizeNumberEncoder }, -): FixedSizeEncoder, TSize>; -export function getScalarEnumEncoder( - constructor: TEnum, - config?: ScalarEnumCodecConfig, -): VariableSizeEncoder>; -export function getScalarEnumEncoder( - constructor: TEnum, - config: ScalarEnumCodecConfig = {}, -): Encoder> { - const prefix = config.size ?? getU8Encoder(); - const { minRange, maxRange, allStringInputs, enumKeys, enumValues } = getScalarEnumStats(constructor); - return mapEncoder(prefix, (value: ScalarEnumFrom): number => { - const isInvalidNumber = typeof value === 'number' && (value < minRange || value > maxRange); - const isInvalidString = typeof value === 'string' && !allStringInputs.includes(value); - if (isInvalidNumber || isInvalidString) { - throw new SolanaError(SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, { - maxRange, - minRange, - value, - variants: allStringInputs, - }); - } - if (typeof value === 'number') return value; - const valueIndex = enumValues.indexOf(value as string); - if (valueIndex >= 0) return valueIndex; - return enumKeys.indexOf(value as string); - }); -} - -/** - * Creates a scalar enum decoder. - * - * @param constructor - The constructor of the scalar enum. - * @param config - A set of config for the decoder. - */ -export function getScalarEnumDecoder( - constructor: TEnum, -): FixedSizeDecoder, 1>; -export function getScalarEnumDecoder( - constructor: TEnum, - config: ScalarEnumCodecConfig & { size: FixedSizeNumberDecoder }, -): FixedSizeDecoder, TSize>; -export function getScalarEnumDecoder( - constructor: TEnum, - config?: ScalarEnumCodecConfig, -): VariableSizeDecoder>; -export function getScalarEnumDecoder( - constructor: TEnum, - config: ScalarEnumCodecConfig = {}, -): Decoder> { - const prefix = config.size ?? getU8Decoder(); - const { minRange, maxRange, enumKeys } = getScalarEnumStats(constructor); - return mapDecoder(prefix, (value: bigint | number): ScalarEnumTo => { - const valueAsNumber = Number(value); - if (valueAsNumber < minRange || valueAsNumber > maxRange) { - throw new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, { - discriminator: valueAsNumber, - maxRange, - minRange, - }); - } - return constructor[enumKeys[valueAsNumber]] as ScalarEnumTo; - }); -} - -/** - * Creates a scalar enum codec. - * - * @param constructor - The constructor of the scalar enum. - * @param config - A set of config for the codec. - */ -export function getScalarEnumCodec( - constructor: TEnum, -): FixedSizeCodec, ScalarEnumTo, 1>; -export function getScalarEnumCodec( - constructor: TEnum, - config: ScalarEnumCodecConfig & { size: FixedSizeNumberCodec }, -): FixedSizeCodec, ScalarEnumTo, TSize>; -export function getScalarEnumCodec( - constructor: TEnum, - config?: ScalarEnumCodecConfig, -): VariableSizeCodec, ScalarEnumTo>; -export function getScalarEnumCodec( - constructor: TEnum, - config: ScalarEnumCodecConfig = {}, -): Codec, ScalarEnumTo> { - return combineCodec(getScalarEnumEncoder(constructor, config), getScalarEnumDecoder(constructor, config)); -} - -function getScalarEnumStats( - constructor: TEnum, -): { - allStringInputs: string[]; - enumKeys: string[]; - enumValues: (number | string)[]; - maxRange: number; - minRange: number; -} { - const numericValues = Object.values(constructor).filter(v => typeof v === 'number') as number[]; - const deduplicatedConstructor = Object.fromEntries( - Object.entries(constructor).slice(numericValues.length), - ) as Record; - const enumKeys = Object.keys(deduplicatedConstructor); - const enumValues = Object.values(deduplicatedConstructor); - const minRange = 0; - const maxRange = enumValues.length - 1; - const allStringInputs: string[] = [ - ...new Set([...enumKeys, ...enumValues.filter((v): v is string => typeof v === 'string')]), - ]; - - return { - allStringInputs, - enumKeys, - enumValues, - maxRange, - minRange, - }; -} diff --git a/packages/codecs/README.md b/packages/codecs/README.md index 64af20aea116..431e2f8eda26 100644 --- a/packages/codecs/README.md +++ b/packages/codecs/README.md @@ -81,7 +81,7 @@ The `@solana/codecs` package is composed of several smaller packages, each with - [Map codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#map-codec). - [Tuple codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#tuple-codec). - [Struct codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#struct-codec). - - [Scalar enum codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#scalar-enum-codec). + - [Enum codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#enum-codec). - [Discriminated union codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#discriminated-union-codec). - [Boolean codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#boolean-codec). - [Nullable codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#nullable-codec). diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index a622fcc0bc50..3545322397d5 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -254,7 +254,7 @@ export const SOLANA_ERROR__CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH = 8078006 a export const SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS = 8078007 as const; export const SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE = 8078008 as const; export const SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT = 8078009 as const; -export const SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT = 8078010 as const; +export const SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT = 8078010 as const; export const SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE = 8078011 as const; export const SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE = 8078012 as const; export const SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH = 8078013 as const; @@ -328,8 +328,8 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__CODECS__EXPECTED_VARIABLE_LENGTH | typeof SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH | typeof SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT + | typeof SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT | typeof SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS - | typeof SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT | typeof SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE | typeof SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE | typeof SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 2ea2897494be..52103591f2e3 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -19,8 +19,8 @@ import { SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT, + SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS, - SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE, SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE, SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, @@ -284,17 +284,17 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< value: number | string | symbol; variants: (number | string | symbol)[]; }; - [SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS]: { - actual: bigint | number; - codecDescription: string; - expected: bigint | number; - }; - [SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT]: { + [SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT]: { maxRange: number; minRange: number; value: number | string; variants: string[]; }; + [SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS]: { + actual: bigint | number; + codecDescription: string; + expected: bigint | number; + }; [SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE]: { alphabet: string; base: number; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 85a42d0a9c39..d95214b54c3b 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -27,8 +27,8 @@ import { SOLANA_ERROR__CODECS__EXPECTED_VARIABLE_LENGTH, SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT, + SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS, - SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE, SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE, SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, @@ -267,10 +267,10 @@ export const SolanaErrorMessages: Readonly<{ 'Codec [$codecDescription] expected $expected bytes, got $bytesLength.', [SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT]: 'Invalid discriminated union variant. Expected one of [$variants], got $value.', + [SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT]: + 'Invalid enum variant. Expected one of [$variants] or a number between $minRange and $maxRange, got $value.', [SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS]: 'Expected [$codecDescription] to have $expected items, got $actual.', - [SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT]: - 'Invalid scalar enum variant. Expected one of [$variants] or a number between $minRange and $maxRange, got $value.', [SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE]: 'Invalid value $value for base $base with alphabet $alphabet.', [SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE]: 'Codec [$codecDescription] expected number to be in the range [$min, $max], got $value.',