From 4accd34f0a70d360321c42f395a2ad45cbadca16 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 19 Jun 2023 18:06:00 +0100 Subject: [PATCH] Extract serializer modules and use in core umi library (#68) * Create new packages for sub-libraries * Extract umi-options package * Extract umi-serializers-core * Extract umi-serializers-encodings * Extract umi-public-keys * Extract umi-serializers-numbers * Refactor toDataView helper * Create numberFactory helper * Refactor integer tests * Refactor other tests * Extract each number serializer into its own file * Cleanup unused test helpers * Extract umi-serializers * Refactor tests * Remove option and pubkey code from main umi lib * Extract serializers and export as sub-path * Import serializer options from lib * Re-exports subset of serializers for backwards compatibility And mark them as deprecated * Remove unused export * Fix tests * Delegate dataViewSerializer to umi-serializers * Add deprecation notices for serializer interfaces * Fix test and linting * Add serializers submodule to externals of consumers * Add changeset * Add OptionOrNullable type --- .changeset/brown-ties-mix.md | 14 + packages/umi-options/README.md | 9 + packages/umi-options/babel.config.json | 3 + packages/umi-options/package.json | 55 +++ packages/umi-options/rollup.config.js | 16 + packages/umi-options/src/common.ts | 81 ++++ packages/umi-options/src/index.ts | 3 + packages/umi-options/src/unwrapOption.ts | 50 +++ .../src/unwrapOptionRecursively.ts | 65 +++ packages/umi-options/test/common.test.ts | 26 ++ packages/umi-options/test/tsconfig.json | 11 + .../umi-options/test/unwrapOption.test.ts | 36 ++ .../test/unwrapOptionRecursively.test.ts} | 70 +-- packages/umi-options/tsconfig.json | 8 + packages/umi-public-keys/README.md | 9 + packages/umi-public-keys/babel.config.json | 3 + packages/umi-public-keys/package.json | 58 +++ packages/umi-public-keys/rollup.config.js | 16 + .../src/common.ts} | 22 +- .../src/errors.ts} | 4 +- packages/umi-public-keys/src/index.ts | 2 + .../test/common.test.ts} | 9 +- packages/umi-public-keys/test/tsconfig.json | 11 + packages/umi-public-keys/tsconfig.json | 8 + packages/umi-serializer-beet/README.md | 2 + .../umi-serializer-beet/test/bytes.test.ts | 2 +- .../umi-serializer-beet/test/empty.test.ts | 3 +- packages/umi-serializer-data-view/README.md | 2 + .../umi-serializer-data-view/rollup.config.js | 1 + .../src/createDataViewSerializer.ts | 80 +--- .../umi-serializer-data-view/src/dataEnum.ts | 81 ---- .../umi-serializer-data-view/src/errors.ts | 59 --- .../src/getResolvedSize.ts | 39 -- .../src/getSizeDescription.ts | 7 - .../src/getSizeFromChildren.ts | 12 - .../src/getSizePrefix.ts | 8 - .../umi-serializer-data-view/src/helpers.ts | 12 - .../umi-serializer-data-view/src/index.ts | 46 +- .../umi-serializer-data-view/src/numbers.ts | 413 ------------------ packages/umi-serializer-data-view/src/unit.ts | 11 - .../test/empty.test.ts | 124 ------ .../test/numbers.test.ts | 332 -------------- .../test/plugin.test.ts | 8 + packages/umi-serializers-core/README.md | 9 + .../umi-serializers-core/babel.config.json | 3 + packages/umi-serializers-core/package.json | 55 +++ .../umi-serializers-core/rollup.config.js | 16 + packages/umi-serializers-core/src/bytes.ts | 35 ++ packages/umi-serializers-core/src/common.ts | 43 ++ packages/umi-serializers-core/src/errors.ts | 33 ++ .../umi-serializers-core/src/fixSerializer.ts | 45 ++ packages/umi-serializers-core/src/index.ts | 6 + .../umi-serializers-core/src/mapSerializer.ts | 43 ++ .../src/reverseSerializer.ts | 30 ++ packages/umi-serializers-core/test/_setup.ts | 37 ++ .../umi-serializers-core/test/bytes.test.ts | 34 ++ .../umi-serializers-core/test/common.test.ts | 37 ++ .../test/fixSerializer.test.ts | 66 +++ .../test/mapSerializer.test.ts} | 106 +---- .../test/reverseSerializer.test.ts | 38 ++ .../umi-serializers-core/test/tsconfig.json | 11 + packages/umi-serializers-core/tsconfig.json | 8 + packages/umi-serializers-encodings/README.md | 9 + .../babel.config.json | 3 + .../umi-serializers-encodings/package.json | 58 +++ .../rollup.config.js | 16 + .../umi-serializers-encodings/src/base10.ts | 8 + .../umi-serializers-encodings/src/base16.ts | 28 ++ .../umi-serializers-encodings/src/base58.ts | 10 + .../umi-serializers-encodings/src/base64.ts | 28 ++ .../umi-serializers-encodings/src/baseX.ts | 72 +++ .../umi-serializers-encodings/src/errors.ts | 12 + .../umi-serializers-encodings/src/index.ts | 8 + .../src}/nullCharacters.ts | 0 .../umi-serializers-encodings/src/utf8.ts | 20 + .../test/base10.test.ts | 30 ++ .../test/base16.test.ts | 29 ++ .../test/base58.test.ts | 42 ++ .../test/base64.test.ts | 48 ++ .../test/tsconfig.json | 11 + .../test/utf8.test.ts | 22 + .../umi-serializers-encodings/tsconfig.json | 8 + packages/umi-serializers-numbers/README.md | 9 + .../umi-serializers-numbers/babel.config.json | 3 + packages/umi-serializers-numbers/package.json | 58 +++ .../umi-serializers-numbers/rollup.config.js | 16 + .../umi-serializers-numbers/src/common.ts | 39 ++ .../umi-serializers-numbers/src/errors.ts | 15 + packages/umi-serializers-numbers/src/f32.ts | 14 + packages/umi-serializers-numbers/src/f64.ts | 14 + packages/umi-serializers-numbers/src/i128.ts | 31 ++ packages/umi-serializers-numbers/src/i16.ts | 15 + packages/umi-serializers-numbers/src/i32.ts | 15 + packages/umi-serializers-numbers/src/i64.ts | 15 + packages/umi-serializers-numbers/src/i8.ts | 15 + packages/umi-serializers-numbers/src/index.ts | 15 + .../umi-serializers-numbers/src/shortU16.ts | 68 +++ packages/umi-serializers-numbers/src/u128.ts | 28 ++ packages/umi-serializers-numbers/src/u16.ts | 15 + packages/umi-serializers-numbers/src/u32.ts | 15 + packages/umi-serializers-numbers/src/u64.ts | 15 + packages/umi-serializers-numbers/src/u8.ts | 15 + packages/umi-serializers-numbers/src/utils.ts | 102 +++++ .../umi-serializers-numbers/test/_setup.ts | 59 +++ .../umi-serializers-numbers/test/f32.test.ts | 39 ++ .../umi-serializers-numbers/test/f64.test.ts | 39 ++ .../umi-serializers-numbers/test/i128.test.ts | 52 +++ .../umi-serializers-numbers/test/i16.test.ts | 52 +++ .../umi-serializers-numbers/test/i32.test.ts | 52 +++ .../umi-serializers-numbers/test/i64.test.ts | 52 +++ .../umi-serializers-numbers/test/i8.test.ts | 36 ++ .../test/shortU16.test.ts | 45 ++ .../test/tsconfig.json | 11 + .../umi-serializers-numbers/test/u128.test.ts | 51 +++ .../umi-serializers-numbers/test/u16.test.ts | 51 +++ .../umi-serializers-numbers/test/u32.test.ts | 51 +++ .../umi-serializers-numbers/test/u64.test.ts | 51 +++ .../umi-serializers-numbers/test/u8.test.ts | 33 ++ .../umi-serializers-numbers/tsconfig.json | 8 + packages/umi-serializers/README.md | 9 + packages/umi-serializers/babel.config.json | 3 + packages/umi-serializers/package.json | 62 +++ packages/umi-serializers/rollup.config.js | 16 + .../src/array.ts | 50 ++- .../src/arrayLikeSerializerSize.ts | 16 + packages/umi-serializers/src/bitArray.ts | 82 ++++ .../src/bool.ts | 32 +- .../src/bytes.ts | 33 +- packages/umi-serializers/src/dataEnum.ts | 170 +++++++ packages/umi-serializers/src/errors.ts | 82 ++++ packages/umi-serializers/src/index.ts | 24 + .../src/map.ts | 49 ++- .../src/maxSerializerSizes.ts | 0 .../src/nullable.ts | 48 +- .../src/option.ts | 60 ++- .../src/publicKey.ts} | 23 +- .../src/scalarEnum.ts | 69 ++- .../src/set.ts | 48 +- .../src/string.ts | 42 +- .../src/struct.ts | 28 +- .../src/sumSerializerSizes.ts | 0 .../src/tuple.ts | 25 +- packages/umi-serializers/src/unit.ts | 25 ++ packages/umi-serializers/src/utils.ts | 61 +++ .../test/_helpers.ts | 2 +- .../test/array.test.ts | 13 +- .../umi-serializers/test/bitArray.test.ts | 63 +++ .../test/bool.test.ts | 6 +- .../test/bytes.test.ts | 8 +- .../test/dataEnum.test.ts | 32 +- packages/umi-serializers/test/empty.test.ts | 96 ++++ .../test/map.test.ts | 11 +- .../test/nullable.test.ts | 4 +- .../test/option.test.ts | 8 +- .../test/publicKey.test.ts | 10 +- .../test/scalarEnum.test.ts} | 30 +- .../test/set.test.ts | 11 +- .../test/string.test.ts | 6 +- .../test/struct.test.ts | 7 +- packages/umi-serializers/test/tsconfig.json | 11 + .../test/tuples.test.ts | 6 +- .../test/unit.test.ts | 4 +- packages/umi-serializers/tsconfig.json | 8 + .../package.json | 3 +- .../rollup.config.js | 1 + .../src/createWeb3JsTransactionFactory.ts | 102 ++--- .../src/plugin.ts | 2 +- .../test/_setup.ts | 12 +- packages/umi/package.json | 13 +- packages/umi/rollup.config.js | 45 +- packages/umi/serializers.d.ts | 2 + packages/umi/src/Account.ts | 4 +- packages/umi/src/Amount.ts | 6 +- packages/umi/src/Context.ts | 6 +- packages/umi/src/DateTime.ts | 6 +- packages/umi/src/EddsaInterface.ts | 2 +- packages/umi/src/GenericFile.ts | 3 +- packages/umi/src/GpaBuilder.ts | 10 +- packages/umi/src/Instruction.ts | 2 +- packages/umi/src/Keypair.ts | 2 +- packages/umi/src/Option.ts | 185 -------- packages/umi/src/Program.ts | 2 +- .../umi/src/ProgramRepositoryInterface.ts | 7 +- packages/umi/src/RpcInterface.ts | 4 +- packages/umi/src/Serializer.ts | 152 ------- packages/umi/src/SerializerInterface.ts | 285 ++---------- packages/umi/src/Signer.ts | 5 +- packages/umi/src/Transaction.ts | 2 +- .../umi/src/errors/AccountNotFoundError.ts | 2 +- .../umi/src/errors/UnexpectedAccountError.ts | 2 +- packages/umi/src/errors/index.ts | 1 - packages/umi/src/index.ts | 11 +- packages/umi/src/serializers.ts | 1 + packages/umi/src/serializersInternal.ts | 190 ++++++++ packages/umi/src/utils/bytes.ts | 254 ----------- packages/umi/src/utils/index.ts | 2 - packages/umi/test/GpaBuilder.test.ts | 3 +- packages/umi/test/_setup.ts | 10 +- packages/umi/test/utils/bytes.test.ts | 224 ---------- pnpm-lock.yaml | 95 +++- rollup.config.js | 3 +- 201 files changed, 4461 insertions(+), 2755 deletions(-) create mode 100644 .changeset/brown-ties-mix.md create mode 100644 packages/umi-options/README.md create mode 100644 packages/umi-options/babel.config.json create mode 100644 packages/umi-options/package.json create mode 100644 packages/umi-options/rollup.config.js create mode 100644 packages/umi-options/src/common.ts create mode 100644 packages/umi-options/src/index.ts create mode 100644 packages/umi-options/src/unwrapOption.ts create mode 100644 packages/umi-options/src/unwrapOptionRecursively.ts create mode 100644 packages/umi-options/test/common.test.ts create mode 100644 packages/umi-options/test/tsconfig.json create mode 100644 packages/umi-options/test/unwrapOption.test.ts rename packages/{umi/test/Option.test.ts => umi-options/test/unwrapOptionRecursively.test.ts} (72%) create mode 100644 packages/umi-options/tsconfig.json create mode 100644 packages/umi-public-keys/README.md create mode 100644 packages/umi-public-keys/babel.config.json create mode 100644 packages/umi-public-keys/package.json create mode 100644 packages/umi-public-keys/rollup.config.js rename packages/{umi/src/PublicKey.ts => umi-public-keys/src/common.ts} (98%) rename packages/{umi/src/errors/InvalidPublicKeyError.ts => umi-public-keys/src/errors.ts} (75%) create mode 100644 packages/umi-public-keys/src/index.ts rename packages/{umi/test/PublicKey.test.ts => umi-public-keys/test/common.test.ts} (92%) create mode 100644 packages/umi-public-keys/test/tsconfig.json create mode 100644 packages/umi-public-keys/tsconfig.json delete mode 100644 packages/umi-serializer-data-view/src/dataEnum.ts delete mode 100644 packages/umi-serializer-data-view/src/errors.ts delete mode 100644 packages/umi-serializer-data-view/src/getResolvedSize.ts delete mode 100644 packages/umi-serializer-data-view/src/getSizeDescription.ts delete mode 100644 packages/umi-serializer-data-view/src/getSizeFromChildren.ts delete mode 100644 packages/umi-serializer-data-view/src/getSizePrefix.ts delete mode 100644 packages/umi-serializer-data-view/src/helpers.ts delete mode 100644 packages/umi-serializer-data-view/src/numbers.ts delete mode 100644 packages/umi-serializer-data-view/src/unit.ts delete mode 100644 packages/umi-serializer-data-view/test/empty.test.ts delete mode 100644 packages/umi-serializer-data-view/test/numbers.test.ts create mode 100644 packages/umi-serializer-data-view/test/plugin.test.ts create mode 100644 packages/umi-serializers-core/README.md create mode 100644 packages/umi-serializers-core/babel.config.json create mode 100644 packages/umi-serializers-core/package.json create mode 100644 packages/umi-serializers-core/rollup.config.js create mode 100644 packages/umi-serializers-core/src/bytes.ts create mode 100644 packages/umi-serializers-core/src/common.ts create mode 100644 packages/umi-serializers-core/src/errors.ts create mode 100644 packages/umi-serializers-core/src/fixSerializer.ts create mode 100644 packages/umi-serializers-core/src/index.ts create mode 100644 packages/umi-serializers-core/src/mapSerializer.ts create mode 100644 packages/umi-serializers-core/src/reverseSerializer.ts create mode 100644 packages/umi-serializers-core/test/_setup.ts create mode 100644 packages/umi-serializers-core/test/bytes.test.ts create mode 100644 packages/umi-serializers-core/test/common.test.ts create mode 100644 packages/umi-serializers-core/test/fixSerializer.test.ts rename packages/{umi/test/Serializer.test.ts => umi-serializers-core/test/mapSerializer.test.ts} (56%) create mode 100644 packages/umi-serializers-core/test/reverseSerializer.test.ts create mode 100644 packages/umi-serializers-core/test/tsconfig.json create mode 100644 packages/umi-serializers-core/tsconfig.json create mode 100644 packages/umi-serializers-encodings/README.md create mode 100644 packages/umi-serializers-encodings/babel.config.json create mode 100644 packages/umi-serializers-encodings/package.json create mode 100644 packages/umi-serializers-encodings/rollup.config.js create mode 100644 packages/umi-serializers-encodings/src/base10.ts create mode 100644 packages/umi-serializers-encodings/src/base16.ts create mode 100644 packages/umi-serializers-encodings/src/base58.ts create mode 100644 packages/umi-serializers-encodings/src/base64.ts create mode 100644 packages/umi-serializers-encodings/src/baseX.ts create mode 100644 packages/umi-serializers-encodings/src/errors.ts create mode 100644 packages/umi-serializers-encodings/src/index.ts rename packages/{umi/src/utils => umi-serializers-encodings/src}/nullCharacters.ts (100%) create mode 100644 packages/umi-serializers-encodings/src/utf8.ts create mode 100644 packages/umi-serializers-encodings/test/base10.test.ts create mode 100644 packages/umi-serializers-encodings/test/base16.test.ts create mode 100644 packages/umi-serializers-encodings/test/base58.test.ts create mode 100644 packages/umi-serializers-encodings/test/base64.test.ts create mode 100644 packages/umi-serializers-encodings/test/tsconfig.json create mode 100644 packages/umi-serializers-encodings/test/utf8.test.ts create mode 100644 packages/umi-serializers-encodings/tsconfig.json create mode 100644 packages/umi-serializers-numbers/README.md create mode 100644 packages/umi-serializers-numbers/babel.config.json create mode 100644 packages/umi-serializers-numbers/package.json create mode 100644 packages/umi-serializers-numbers/rollup.config.js create mode 100644 packages/umi-serializers-numbers/src/common.ts create mode 100644 packages/umi-serializers-numbers/src/errors.ts create mode 100644 packages/umi-serializers-numbers/src/f32.ts create mode 100644 packages/umi-serializers-numbers/src/f64.ts create mode 100644 packages/umi-serializers-numbers/src/i128.ts create mode 100644 packages/umi-serializers-numbers/src/i16.ts create mode 100644 packages/umi-serializers-numbers/src/i32.ts create mode 100644 packages/umi-serializers-numbers/src/i64.ts create mode 100644 packages/umi-serializers-numbers/src/i8.ts create mode 100644 packages/umi-serializers-numbers/src/index.ts create mode 100644 packages/umi-serializers-numbers/src/shortU16.ts create mode 100644 packages/umi-serializers-numbers/src/u128.ts create mode 100644 packages/umi-serializers-numbers/src/u16.ts create mode 100644 packages/umi-serializers-numbers/src/u32.ts create mode 100644 packages/umi-serializers-numbers/src/u64.ts create mode 100644 packages/umi-serializers-numbers/src/u8.ts create mode 100644 packages/umi-serializers-numbers/src/utils.ts create mode 100644 packages/umi-serializers-numbers/test/_setup.ts create mode 100644 packages/umi-serializers-numbers/test/f32.test.ts create mode 100644 packages/umi-serializers-numbers/test/f64.test.ts create mode 100644 packages/umi-serializers-numbers/test/i128.test.ts create mode 100644 packages/umi-serializers-numbers/test/i16.test.ts create mode 100644 packages/umi-serializers-numbers/test/i32.test.ts create mode 100644 packages/umi-serializers-numbers/test/i64.test.ts create mode 100644 packages/umi-serializers-numbers/test/i8.test.ts create mode 100644 packages/umi-serializers-numbers/test/shortU16.test.ts create mode 100644 packages/umi-serializers-numbers/test/tsconfig.json create mode 100644 packages/umi-serializers-numbers/test/u128.test.ts create mode 100644 packages/umi-serializers-numbers/test/u16.test.ts create mode 100644 packages/umi-serializers-numbers/test/u32.test.ts create mode 100644 packages/umi-serializers-numbers/test/u64.test.ts create mode 100644 packages/umi-serializers-numbers/test/u8.test.ts create mode 100644 packages/umi-serializers-numbers/tsconfig.json create mode 100644 packages/umi-serializers/README.md create mode 100644 packages/umi-serializers/babel.config.json create mode 100644 packages/umi-serializers/package.json create mode 100644 packages/umi-serializers/rollup.config.js rename packages/{umi-serializer-data-view => umi-serializers}/src/array.ts (58%) create mode 100644 packages/umi-serializers/src/arrayLikeSerializerSize.ts create mode 100644 packages/umi-serializers/src/bitArray.ts rename packages/{umi-serializer-data-view => umi-serializers}/src/bool.ts (53%) rename packages/{umi-serializer-data-view => umi-serializers}/src/bytes.ts (64%) create mode 100644 packages/umi-serializers/src/dataEnum.ts create mode 100644 packages/umi-serializers/src/errors.ts create mode 100644 packages/umi-serializers/src/index.ts rename packages/{umi-serializer-data-view => umi-serializers}/src/map.ts (63%) rename packages/{umi-serializer-data-view => umi-serializers}/src/maxSerializerSizes.ts (100%) rename packages/{umi-serializer-data-view => umi-serializers}/src/nullable.ts (62%) rename packages/{umi-serializer-data-view => umi-serializers}/src/option.ts (61%) rename packages/{umi-serializer-data-view/src/pubkey.ts => umi-serializers/src/publicKey.ts} (64%) rename packages/{umi-serializer-data-view => umi-serializers}/src/scalarEnum.ts (59%) rename packages/{umi-serializer-data-view => umi-serializers}/src/set.ts (58%) rename packages/{umi-serializer-data-view => umi-serializers}/src/string.ts (57%) rename packages/{umi-serializer-data-view => umi-serializers}/src/struct.ts (63%) rename packages/{umi-serializer-data-view => umi-serializers}/src/sumSerializerSizes.ts (100%) rename packages/{umi-serializer-data-view => umi-serializers}/src/tuple.ts (64%) create mode 100644 packages/umi-serializers/src/unit.ts create mode 100644 packages/umi-serializers/src/utils.ts rename packages/{umi-serializer-data-view => umi-serializers}/test/_helpers.ts (93%) rename packages/{umi-serializer-data-view => umi-serializers}/test/array.test.ts (90%) create mode 100644 packages/umi-serializers/test/bitArray.test.ts rename packages/{umi-serializer-data-view => umi-serializers}/test/bool.test.ts (85%) rename packages/{umi-serializer-data-view => umi-serializers}/test/bytes.test.ts (89%) rename packages/{umi-serializer-data-view => umi-serializers}/test/dataEnum.test.ts (89%) create mode 100644 packages/umi-serializers/test/empty.test.ts rename packages/{umi-serializer-data-view => umi-serializers}/test/map.test.ts (93%) rename packages/{umi-serializer-data-view => umi-serializers}/test/nullable.test.ts (96%) rename packages/{umi-serializer-data-view => umi-serializers}/test/option.test.ts (96%) rename packages/{umi-serializer-data-view => umi-serializers}/test/publicKey.test.ts (91%) rename packages/{umi-serializer-data-view/test/enum.test.ts => umi-serializers/test/scalarEnum.test.ts} (84%) rename packages/{umi-serializer-data-view => umi-serializers}/test/set.test.ts (92%) rename packages/{umi-serializer-data-view => umi-serializers}/test/string.test.ts (95%) rename packages/{umi-serializer-data-view => umi-serializers}/test/struct.test.ts (89%) create mode 100644 packages/umi-serializers/test/tsconfig.json rename packages/{umi-serializer-data-view => umi-serializers}/test/tuples.test.ts (90%) rename packages/{umi-serializer-data-view => umi-serializers}/test/unit.test.ts (87%) create mode 100644 packages/umi-serializers/tsconfig.json create mode 100644 packages/umi/serializers.d.ts delete mode 100644 packages/umi/src/Option.ts delete mode 100644 packages/umi/src/Serializer.ts create mode 100644 packages/umi/src/serializers.ts create mode 100644 packages/umi/src/serializersInternal.ts delete mode 100644 packages/umi/src/utils/bytes.ts delete mode 100644 packages/umi/test/utils/bytes.test.ts diff --git a/.changeset/brown-ties-mix.md b/.changeset/brown-ties-mix.md new file mode 100644 index 00000000..85e8a3b3 --- /dev/null +++ b/.changeset/brown-ties-mix.md @@ -0,0 +1,14 @@ +--- +'@metaplex-foundation/umi-transaction-factory-web3js': patch +'@metaplex-foundation/umi-serializers-encodings': patch +'@metaplex-foundation/umi-serializer-data-view': patch +'@metaplex-foundation/umi-serializers-numbers': patch +'@metaplex-foundation/umi-serializers-core': patch +'@metaplex-foundation/umi-serializer-beet': patch +'@metaplex-foundation/umi-public-keys': patch +'@metaplex-foundation/umi-serializers': patch +'@metaplex-foundation/umi-options': patch +'@metaplex-foundation/umi': patch +--- + +Extract serializer modules and use in core umi library diff --git a/packages/umi-options/README.md b/packages/umi-options/README.md new file mode 100644 index 00000000..93b3436a --- /dev/null +++ b/packages/umi-options/README.md @@ -0,0 +1,9 @@ +# umi-options + +A TypeScript implementation of Rust Options + +## Installation + +```sh +npm install @metaplex-foundation/umi-options +``` diff --git a/packages/umi-options/babel.config.json b/packages/umi-options/babel.config.json new file mode 100644 index 00000000..ac08da0a --- /dev/null +++ b/packages/umi-options/babel.config.json @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} diff --git a/packages/umi-options/package.json b/packages/umi-options/package.json new file mode 100644 index 00000000..119c12f0 --- /dev/null +++ b/packages/umi-options/package.json @@ -0,0 +1,55 @@ +{ + "name": "@metaplex-foundation/umi-options", + "version": "0.0.1", + "description": "A TypeScript implementation of Rust Options", + "license": "MIT", + "sideEffects": false, + "module": "dist/esm/index.mjs", + "main": "dist/cjs/index.cjs", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, + "files": [ + "/dist/cjs", + "/dist/esm", + "/dist/types", + "/src" + ], + "scripts": { + "lint": "eslint --ext js,ts,tsx src", + "lint:fix": "eslint --fix --ext js,ts,tsx src", + "clean": "rimraf dist", + "build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c", + "test": "ava" + }, + "devDependencies": { + "@ava/typescript": "^3.0.1", + "ava": "^5.1.0" + }, + "publishConfig": { + "access": "public" + }, + "author": "Metaplex Maintainers ", + "homepage": "https://metaplex.com", + "repository": { + "url": "https://github.com/metaplex-foundation/umi.git" + }, + "typedoc": { + "entryPoint": "./src/index.ts", + "readmeFile": "./README.md", + "displayName": "umi-options" + }, + "ava": { + "typescript": { + "compile": false, + "rewritePaths": { + "src/": "dist/test/src/", + "test/": "dist/test/test/" + } + } + } +} diff --git a/packages/umi-options/rollup.config.js b/packages/umi-options/rollup.config.js new file mode 100644 index 00000000..ba38fd10 --- /dev/null +++ b/packages/umi-options/rollup.config.js @@ -0,0 +1,16 @@ +import { createConfigs } from '../../rollup.config'; +import pkg from './package.json'; + +export default createConfigs({ + pkg, + builds: [ + { + dir: 'dist/esm', + format: 'es', + }, + { + dir: 'dist/cjs', + format: 'cjs', + }, + ], +}); diff --git a/packages/umi-options/src/common.ts b/packages/umi-options/src/common.ts new file mode 100644 index 00000000..ea620046 --- /dev/null +++ b/packages/umi-options/src/common.ts @@ -0,0 +1,81 @@ +/** + * Defines a type `T` that can also be `null`. + * @category Utils — Options + */ +export type Nullable = T | null; + +/** + * An implementation of the Rust Option type in JavaScript. + * It can be one of the following: + * - {@link Some}: Meaning there is a value of type T. + * - {@link None}: Meaning there is no value. + * + * @category Utils — Options + */ +export type Option = Some | None; + +/** + * Defines a looser type that can be used when serializing an {@link Option}. + * This allows us to pass null or the Option value directly whilst still + * supporting the Option type for use-cases that need more type safety. + * + * @category Utils — Options + */ +export type OptionOrNullable = Option | Nullable; + +/** + * Represents an option of type `T` that has a value. + * + * @see {@link Option} + * @category Utils — Options + */ +export type Some = { __option: 'Some'; value: T }; + +/** + * Represents an option of type `T` that has no value. + * + * @see {@link Option} + * @category Utils — Options + */ +export type None = { __option: 'None' }; + +/** + * Creates a new {@link Option} of type `T` that has a value. + * + * @see {@link Option} + * @category Utils — Options + */ +export const some = (value: T): Option => ({ __option: 'Some', value }); + +/** + * Creates a new {@link Option} of type `T` that has no value. + * + * @see {@link Option} + * @category Utils — Options + */ +export const none = (): Option => ({ __option: 'None' }); + +/** + * Whether the given data is an {@link Option}. + * @category Utils — Options + */ +export const isOption = (input: any): input is Option => + input && + typeof input === 'object' && + '__option' in input && + ((input.__option === 'Some' && 'value' in input) || + input.__option === 'None'); + +/** + * Whether the given {@link Option} is a {@link Some}. + * @category Utils — Options + */ +export const isSome = (option: Option): option is Some => + option.__option === 'Some'; + +/** + * Whether the given {@link Option} is a {@link None}. + * @category Utils — Options + */ +export const isNone = (option: Option): option is None => + option.__option === 'None'; diff --git a/packages/umi-options/src/index.ts b/packages/umi-options/src/index.ts new file mode 100644 index 00000000..bcb30029 --- /dev/null +++ b/packages/umi-options/src/index.ts @@ -0,0 +1,3 @@ +export * from './common'; +export * from './unwrapOption'; +export * from './unwrapOptionRecursively'; diff --git a/packages/umi-options/src/unwrapOption.ts b/packages/umi-options/src/unwrapOption.ts new file mode 100644 index 00000000..d29e4ba6 --- /dev/null +++ b/packages/umi-options/src/unwrapOption.ts @@ -0,0 +1,50 @@ +import { Nullable, Option, isSome, none, some } from './common'; + +/** + * Unwraps the value of an {@link Option} of type `T` + * or returns a fallback value that defaults to `null`. + * + * @category Utils — Options + */ +export function unwrapOption(option: Option): Nullable; +export function unwrapOption(option: Option, fallback: () => U): T | U; +export function unwrapOption( + option: Option, + fallback?: () => U +): T | U { + if (isSome(option)) return option.value; + return fallback ? fallback() : (null as U); +} + +/** + * Wraps a nullable value into an {@link Option}. + * + * @category Utils — Options + */ +export const wrapNullable = (nullable: Nullable): Option => + nullable !== null ? some(nullable) : none(); + +/** + * Unwraps the value of an {@link Option} of type `T`. + * If the option is a {@link Some}, it returns its value, + * Otherwise, it returns `null`. + * + * @category Utils — Options + * @deprecated Use {@link unwrapOption} instead. + */ +export const unwrapSome = (option: Option): Nullable => + isSome(option) ? option.value : null; + +/** + * Unwraps the value of an {@link Option} of type `T` + * or returns a custom fallback value. + * If the option is a {@link Some}, it returns its value, + * Otherwise, it returns the return value of the provided fallback callback. + * + * @category Utils — Options + * @deprecated Use {@link unwrapOption} instead. + */ +export const unwrapSomeOrElse = ( + option: Option, + fallback: () => U +): T | U => (isSome(option) ? option.value : fallback()); diff --git a/packages/umi-options/src/unwrapOptionRecursively.ts b/packages/umi-options/src/unwrapOptionRecursively.ts new file mode 100644 index 00000000..b84cd1ac --- /dev/null +++ b/packages/umi-options/src/unwrapOptionRecursively.ts @@ -0,0 +1,65 @@ +import { None, Some, isOption, isSome } from './common'; + +/** + * A type that defines the recursive unwrapping of a type `T` + * such that all nested {@link Option} types are unwrapped. + * + * For each nested {@link Option} type, if the option is a {@link Some}, + * it returns the type of its value, otherwise, it returns the provided + * fallback type `U` which defaults to `null`. + * + * @category Utils — Options + */ +type UnwrappedOption = T extends Some + ? UnwrappedOption + : T extends None + ? U + : T extends object + ? { [key in keyof T]: UnwrappedOption } + : T extends Array + ? Array> + : T; + +/** + * Recursively go through a type `T`such that all + * nested {@link Option} types are unwrapped. + * + * For each nested {@link Option} type, if the option is a {@link Some}, + * it returns its value, otherwise, it returns the provided fallback value + * which defaults to `null`. + * + * @category Utils — Options + */ +export function unwrapOptionRecursively(input: T): UnwrappedOption; +export function unwrapOptionRecursively( + input: T, + fallback: () => U +): UnwrappedOption; +export function unwrapOptionRecursively( + input: T, + fallback?: () => U +): UnwrappedOption { + // Because null passes `typeof input === 'object'`. + if (!input) return input as UnwrappedOption; + const next = (x: X) => + (fallback + ? unwrapOptionRecursively(x, fallback) + : unwrapOptionRecursively(x)) as UnwrappedOption; + + // Handle Option. + if (isOption(input)) { + if (isSome(input)) return next(input.value) as UnwrappedOption; + return (fallback ? fallback() : null) as UnwrappedOption; + } + + // Walk. + if (Array.isArray(input)) { + return input.map(next) as UnwrappedOption; + } + if (typeof input === 'object') { + return Object.fromEntries( + Object.entries(input).map(([k, v]) => [k, next(v)]) + ) as UnwrappedOption; + } + return input as UnwrappedOption; +} diff --git a/packages/umi-options/test/common.test.ts b/packages/umi-options/test/common.test.ts new file mode 100644 index 00000000..5dcf0574 --- /dev/null +++ b/packages/umi-options/test/common.test.ts @@ -0,0 +1,26 @@ +import test from 'ava'; +import { isNone, isSome, none, Option, some } from '../src'; + +test('it can create Some and None options', (t) => { + const optionA: Option = some(42); + t.deepEqual(optionA, { __option: 'Some', value: 42 }); + + const optionB: Option = some(null); + t.deepEqual(optionB, { __option: 'Some', value: null }); + + const optionC: Option = none(); + t.deepEqual(optionC, { __option: 'None' }); + + const optionD: Option = none(); + t.deepEqual(optionD, { __option: 'None' }); +}); + +test('it can check if an option is Some or None', (t) => { + const optionA = some(42); + t.true(isSome(optionA)); + t.false(isNone(optionA)); + + const optionB = none(); + t.false(isSome(optionB)); + t.true(isNone(optionB)); +}); diff --git a/packages/umi-options/test/tsconfig.json b/packages/umi-options/test/tsconfig.json new file mode 100644 index 00000000..2390f598 --- /dev/null +++ b/packages/umi-options/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./**/*"], + "compilerOptions": { + "module": "commonjs", + "outDir": "../dist/test", + "declarationDir": null, + "declaration": false, + "emitDeclarationOnly": false + } +} diff --git a/packages/umi-options/test/unwrapOption.test.ts b/packages/umi-options/test/unwrapOption.test.ts new file mode 100644 index 00000000..a798e495 --- /dev/null +++ b/packages/umi-options/test/unwrapOption.test.ts @@ -0,0 +1,36 @@ +import test from 'ava'; +import { none, some, unwrapOption, wrapNullable } from '../src'; + +test('it can unwrap an Option as a Nullable', (t) => { + t.is(unwrapOption(some(42)), 42); + t.is(unwrapOption(some(null)), null); + t.is(unwrapOption(some('hello')), 'hello'); + t.is(unwrapOption(none()), null); + t.is(unwrapOption(none()), null); + t.is(unwrapOption(none()), null); +}); + +test('it can unwrap an Option using a fallback callback', (t) => { + const fallbackA = () => 42 as const; + t.is(unwrapOption(some(1), fallbackA), 1); + t.is(unwrapOption(some('A'), fallbackA), 'A'); + t.is(unwrapOption(none(), fallbackA), 42); + + const fallbackB = () => { + throw new Error('Fallback Error'); + }; + t.is(unwrapOption(some(1), fallbackB), 1); + t.is(unwrapOption(some('A'), fallbackB), 'A'); + t.throws(() => unwrapOption(none(), fallbackB), { + message: 'Fallback Error', + }); +}); + +test('it can wrap a Nullable as an Option', (t) => { + t.deepEqual(wrapNullable(42), some(42)); + t.deepEqual(wrapNullable('hello'), some('hello')); + t.deepEqual(wrapNullable(false), some(false)); + t.deepEqual(wrapNullable(undefined), some(undefined)); + t.deepEqual(wrapNullable(null), none()); + t.deepEqual(wrapNullable(null), none()); +}); diff --git a/packages/umi/test/Option.test.ts b/packages/umi-options/test/unwrapOptionRecursively.test.ts similarity index 72% rename from packages/umi/test/Option.test.ts rename to packages/umi-options/test/unwrapOptionRecursively.test.ts index 853c3177..facd1ace 100644 --- a/packages/umi/test/Option.test.ts +++ b/packages/umi-options/test/unwrapOptionRecursively.test.ts @@ -1,64 +1,5 @@ import test from 'ava'; -import { - isNone, - isSome, - none, - Nullable, - Option, - some, - unwrapOption, - unwrapOptionRecursively, - wrapNullable, -} from '../src'; - -test('it can create Some and None options', (t) => { - const optionA: Option = some(42); - t.deepEqual(optionA, { __option: 'Some', value: 42 }); - - const optionB: Option = some(null); - t.deepEqual(optionB, { __option: 'Some', value: null }); - - const optionC: Option = none(); - t.deepEqual(optionC, { __option: 'None' }); - - const optionD: Option = none(); - t.deepEqual(optionD, { __option: 'None' }); -}); - -test('it can check if an option is Some or None', (t) => { - const optionA = some(42); - t.true(isSome(optionA)); - t.false(isNone(optionA)); - - const optionB = none(); - t.false(isSome(optionB)); - t.true(isNone(optionB)); -}); - -test('it can unwrap an Option as a Nullable', (t) => { - t.is(unwrapOption(some(42)), 42); - t.is(unwrapOption(some(null)), null); - t.is(unwrapOption(some('hello')), 'hello'); - t.is(unwrapOption(none()), null); - t.is(unwrapOption(none()), null); - t.is(unwrapOption(none()), null); -}); - -test('it can unwrap an Option using a fallback callback', (t) => { - const fallbackA = () => 42 as const; - t.is(unwrapOption(some(1), fallbackA), 1); - t.is(unwrapOption(some('A'), fallbackA), 'A'); - t.is(unwrapOption(none(), fallbackA), 42); - - const fallbackB = () => { - throw new Error('Fallback Error'); - }; - t.is(unwrapOption(some(1), fallbackB), 1); - t.is(unwrapOption(some('A'), fallbackB), 'A'); - t.throws(() => unwrapOption(none(), fallbackB), { - message: 'Fallback Error', - }); -}); +import { none, Nullable, some, unwrapOptionRecursively } from '../src'; test('it can unwrap options recursively', (t) => { // Some. @@ -250,12 +191,3 @@ test('it can unwrap options recursively whilst using a custom fallback', (t) => }, }); }); - -test('it can wrap a Nullable as an Option', (t) => { - t.deepEqual(wrapNullable(42), some(42)); - t.deepEqual(wrapNullable('hello'), some('hello')); - t.deepEqual(wrapNullable(false), some(false)); - t.deepEqual(wrapNullable(undefined), some(undefined)); - t.deepEqual(wrapNullable(null), none()); - t.deepEqual(wrapNullable(null), none()); -}); diff --git a/packages/umi-options/tsconfig.json b/packages/umi-options/tsconfig.json new file mode 100644 index 00000000..89a681d0 --- /dev/null +++ b/packages/umi-options/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist/esm", + "declarationDir": "dist/types" + } +} diff --git a/packages/umi-public-keys/README.md b/packages/umi-public-keys/README.md new file mode 100644 index 00000000..a7c74462 --- /dev/null +++ b/packages/umi-public-keys/README.md @@ -0,0 +1,9 @@ +# umi-public-keys + +Defines public keys for the Umi framework + +## Installation + +```sh +npm install @metaplex-foundation/umi-public-keys +``` diff --git a/packages/umi-public-keys/babel.config.json b/packages/umi-public-keys/babel.config.json new file mode 100644 index 00000000..ac08da0a --- /dev/null +++ b/packages/umi-public-keys/babel.config.json @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} diff --git a/packages/umi-public-keys/package.json b/packages/umi-public-keys/package.json new file mode 100644 index 00000000..846e3f76 --- /dev/null +++ b/packages/umi-public-keys/package.json @@ -0,0 +1,58 @@ +{ + "name": "@metaplex-foundation/umi-public-keys", + "version": "0.0.1", + "description": "Defines public keys for the Umi framework", + "license": "MIT", + "sideEffects": false, + "module": "dist/esm/index.mjs", + "main": "dist/cjs/index.cjs", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, + "files": [ + "/dist/cjs", + "/dist/esm", + "/dist/types", + "/src" + ], + "scripts": { + "lint": "eslint --ext js,ts,tsx src", + "lint:fix": "eslint --fix --ext js,ts,tsx src", + "clean": "rimraf dist", + "build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c", + "test": "ava" + }, + "dependencies": { + "@metaplex-foundation/umi-serializers-encodings": "workspace:^" + }, + "devDependencies": { + "@ava/typescript": "^3.0.1", + "ava": "^5.1.0" + }, + "publishConfig": { + "access": "public" + }, + "author": "Metaplex Maintainers ", + "homepage": "https://metaplex.com", + "repository": { + "url": "https://github.com/metaplex-foundation/umi.git" + }, + "typedoc": { + "entryPoint": "./src/index.ts", + "readmeFile": "./README.md", + "displayName": "umi-public-keys" + }, + "ava": { + "typescript": { + "compile": false, + "rewritePaths": { + "src/": "dist/test/src/", + "test/": "dist/test/test/" + } + } + } +} diff --git a/packages/umi-public-keys/rollup.config.js b/packages/umi-public-keys/rollup.config.js new file mode 100644 index 00000000..ba38fd10 --- /dev/null +++ b/packages/umi-public-keys/rollup.config.js @@ -0,0 +1,16 @@ +import { createConfigs } from '../../rollup.config'; +import pkg from './package.json'; + +export default createConfigs({ + pkg, + builds: [ + { + dir: 'dist/esm', + format: 'es', + }, + { + dir: 'dist/cjs', + format: 'cjs', + }, + ], +}); diff --git a/packages/umi/src/PublicKey.ts b/packages/umi-public-keys/src/common.ts similarity index 98% rename from packages/umi/src/PublicKey.ts rename to packages/umi-public-keys/src/common.ts index e5ff58e0..ebb72de8 100644 --- a/packages/umi/src/PublicKey.ts +++ b/packages/umi-public-keys/src/common.ts @@ -1,5 +1,5 @@ +import { base58 } from '@metaplex-foundation/umi-serializers-encodings'; import { InvalidPublicKeyError } from './errors'; -import { base58 } from './utils'; /** * The amount of bytes in a public key. @@ -169,16 +169,6 @@ export function assertPublicKey( publicKeyBytes(value); } -/** - * Whether the given public keys are the same. - * @category Signers and PublicKeys - * @deprecated Use `left === right` instead now that public keys are base58 strings. - */ -export const samePublicKey = ( - left: PublicKeyInput, - right: PublicKeyInput -): boolean => publicKey(left) === publicKey(right); - /** * Deduplicates the given array of public keys. * @category Signers and PublicKeys @@ -229,3 +219,13 @@ export const publicKeyBytes = (value: string): PublicKeyBytes => { * @deprecated Public keys are now represented directly as base58 strings. */ export const base58PublicKey = (key: PublicKeyInput): string => publicKey(key); + +/** + * Whether the given public keys are the same. + * @category Signers and PublicKeys + * @deprecated Use `left === right` instead now that public keys are base58 strings. + */ +export const samePublicKey = ( + left: PublicKeyInput, + right: PublicKeyInput +): boolean => publicKey(left) === publicKey(right); diff --git a/packages/umi/src/errors/InvalidPublicKeyError.ts b/packages/umi-public-keys/src/errors.ts similarity index 75% rename from packages/umi/src/errors/InvalidPublicKeyError.ts rename to packages/umi-public-keys/src/errors.ts index fe02e125..0fa273ca 100644 --- a/packages/umi/src/errors/InvalidPublicKeyError.ts +++ b/packages/umi-public-keys/src/errors.ts @@ -2,9 +2,9 @@ export class InvalidPublicKeyError extends Error { readonly name: string = 'InvalidPublicKeyError'; - readonly invalidPublicKey: unknown; + readonly invalidPublicKey: string; - constructor(invalidPublicKey: unknown, reason?: string) { + constructor(invalidPublicKey: string, reason?: string) { reason = reason ? `. ${reason}` : ''; super(`The provided public key is invalid: ${invalidPublicKey}${reason}`); this.invalidPublicKey = invalidPublicKey; diff --git a/packages/umi-public-keys/src/index.ts b/packages/umi-public-keys/src/index.ts new file mode 100644 index 00000000..6835af12 --- /dev/null +++ b/packages/umi-public-keys/src/index.ts @@ -0,0 +1,2 @@ +export * from './common'; +export * from './errors'; diff --git a/packages/umi/test/PublicKey.test.ts b/packages/umi-public-keys/test/common.test.ts similarity index 92% rename from packages/umi/test/PublicKey.test.ts rename to packages/umi-public-keys/test/common.test.ts index 70fc950d..5a4b04c2 100644 --- a/packages/umi/test/PublicKey.test.ts +++ b/packages/umi-public-keys/test/common.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { PublicKey, createNoopSigner, publicKey } from '../src'; +import { PublicKey, publicKey } from '../src'; test('it can create PublicKeys from base 58 strings', (t) => { t.is( @@ -37,7 +37,12 @@ test('it can create PublicKeys from other public keys', (t) => { test('it can create PublicKeys from other public key wrappers like Signers', (t) => { const keyA = publicKey('4HM9LW2rm3SR2ZdBiFK3D21ENmQWpqEJEhx1nfgcC3r9'); - const signerA = createNoopSigner(keyA); + const signerA = { + publicKey: keyA, + signMessage: () => {}, + signTransaction: () => {}, + signAllTransaction: () => {}, + }; const keyB = publicKey(signerA); t.is(keyB, signerA.publicKey); }); diff --git a/packages/umi-public-keys/test/tsconfig.json b/packages/umi-public-keys/test/tsconfig.json new file mode 100644 index 00000000..2390f598 --- /dev/null +++ b/packages/umi-public-keys/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./**/*"], + "compilerOptions": { + "module": "commonjs", + "outDir": "../dist/test", + "declarationDir": null, + "declaration": false, + "emitDeclarationOnly": false + } +} diff --git a/packages/umi-public-keys/tsconfig.json b/packages/umi-public-keys/tsconfig.json new file mode 100644 index 00000000..89a681d0 --- /dev/null +++ b/packages/umi-public-keys/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist/esm", + "declarationDir": "dist/types" + } +} diff --git a/packages/umi-serializer-beet/README.md b/packages/umi-serializer-beet/README.md index 2157b797..d4dd3340 100644 --- a/packages/umi-serializer-beet/README.md +++ b/packages/umi-serializer-beet/README.md @@ -1,5 +1,7 @@ # umi-serializer-beet +⚠️ **This plugin is deprecated. You can now directly use `@metaplex-foundation/umi/serializers` instead.** ⚠️ + A serializer implementation relying on Beet. ## Installation diff --git a/packages/umi-serializer-beet/test/bytes.test.ts b/packages/umi-serializer-beet/test/bytes.test.ts index 07551b80..48302eed 100644 --- a/packages/umi-serializer-beet/test/bytes.test.ts +++ b/packages/umi-serializer-beet/test/bytes.test.ts @@ -31,7 +31,7 @@ test('fixed (de)serialization', (t) => { d(t, bytes5, '0102000000', new Uint8Array([1, 2, 0, 0, 0]), 5); d(t, bytes5, ['ff0102000000', 1], new Uint8Array([1, 2, 0, 0, 0]), 6); t.throws(() => bytes5.deserialize(base16.serialize('0102')), { - message: (m) => m.includes('Fixed serializer expected 5 bytes, got 2'), + message: 'Serializer [fixSerializer] expected 5 bytes, got 2.', }); // Too large (truncated). diff --git a/packages/umi-serializer-beet/test/empty.test.ts b/packages/umi-serializer-beet/test/empty.test.ts index 3ba1d1b1..80288dbb 100644 --- a/packages/umi-serializer-beet/test/empty.test.ts +++ b/packages/umi-serializer-beet/test/empty.test.ts @@ -10,8 +10,7 @@ test('it can handle empty buffers', (t) => { const remainder = { size: 'remainder' } as const; const e: ThrowsExpectation = { instanceOf: DeserializingEmptyBufferError }; const fixedError = (expectedBytes: number) => ({ - message: (m: string) => - m.includes(`Fixed serializer expected ${expectedBytes} bytes, got 0.`), + message: `Serializer [fixSerializer] expected ${expectedBytes} bytes, got 0.`, }); const empty = (serializer: Serializer) => serializer.deserialize(new Uint8Array())[0]; diff --git a/packages/umi-serializer-data-view/README.md b/packages/umi-serializer-data-view/README.md index f68241fe..ba7304e5 100644 --- a/packages/umi-serializer-data-view/README.md +++ b/packages/umi-serializer-data-view/README.md @@ -1,5 +1,7 @@ # umi-serializer-data-view +⚠️ **This plugin is deprecated. You can now directly use `@metaplex-foundation/umi/serializers` instead.** ⚠️ + A serializer implementation relying on the native DataView API. ## Installation diff --git a/packages/umi-serializer-data-view/rollup.config.js b/packages/umi-serializer-data-view/rollup.config.js index ba38fd10..98c85ced 100644 --- a/packages/umi-serializer-data-view/rollup.config.js +++ b/packages/umi-serializer-data-view/rollup.config.js @@ -3,6 +3,7 @@ import pkg from './package.json'; export default createConfigs({ pkg, + additionalExternals: ['@metaplex-foundation/umi/serializers'], builds: [ { dir: 'dist/esm', diff --git a/packages/umi-serializer-data-view/src/createDataViewSerializer.ts b/packages/umi-serializer-data-view/src/createDataViewSerializer.ts index 0e4be6f3..f4caa9c5 100644 --- a/packages/umi-serializer-data-view/src/createDataViewSerializer.ts +++ b/packages/umi-serializer-data-view/src/createDataViewSerializer.ts @@ -1,12 +1,9 @@ -import { Serializer, SerializerInterface } from '@metaplex-foundation/umi'; -import { array } from './array'; -import { bool } from './bool'; -import { bytes } from './bytes'; -import { dataEnum } from './dataEnum'; -import { DeserializingEmptyBufferError } from './errors'; -import { map } from './map'; -import { nullable } from './nullable'; +import { SerializerInterface } from '@metaplex-foundation/umi'; import { + array, + bool, + bytes, + dataEnum, f32, f64, i128, @@ -14,67 +11,36 @@ import { i32, i64, i8, + map, + nullable, + option, + publicKey, + scalarEnum, + set, + string, + struct, + tuple, u128, u16, u32, u64, u8, -} from './numbers'; -import { option } from './option'; -import { publicKey } from './pubkey'; -import { scalarEnum } from './scalarEnum'; -import { set } from './set'; -import { string } from './string'; -import { struct } from './struct'; -import { tuple } from './tuple'; -import { unit } from './unit'; + unit, +} from '@metaplex-foundation/umi/serializers'; -export type DataViewSerializerOptions = { - /** @defaultValue `true` */ - tolerateEmptyBuffers?: boolean; -}; - -function getTolerantSerializerFactory< - TSerializerFactory extends (...args: never[]) => Serializer ->(serializerFactory: TSerializerFactory): TSerializerFactory { - return ((...args) => { - const originalSerializer = serializerFactory(...args); - return { - ...originalSerializer, - deserialize(bytes: Uint8Array, offset = 0) { - try { - return originalSerializer.deserialize(bytes, offset); - } catch (e) { - if ( - e instanceof DeserializingEmptyBufferError && - e.toleratedDefaultValue !== undefined - ) { - return [e.toleratedDefaultValue, offset]; - } - throw e; - } - }, - }; - }) as TSerializerFactory; -} +export type DataViewSerializerOptions = {}; export function createDataViewSerializer( + // eslint-disable-next-line @typescript-eslint/no-unused-vars options: DataViewSerializerOptions = {} ): SerializerInterface { - const shouldTolerateEmptyBuffers = options.tolerateEmptyBuffers !== false; return { tuple, - array: shouldTolerateEmptyBuffers - ? getTolerantSerializerFactory(array) - : array, - map: shouldTolerateEmptyBuffers ? getTolerantSerializerFactory(map) : map, - set: shouldTolerateEmptyBuffers ? getTolerantSerializerFactory(set) : set, - option: shouldTolerateEmptyBuffers - ? getTolerantSerializerFactory(option) - : option, - nullable: shouldTolerateEmptyBuffers - ? getTolerantSerializerFactory(nullable) - : nullable, + array, + map, + set, + option, + nullable, struct, enum: scalarEnum, dataEnum, diff --git a/packages/umi-serializer-data-view/src/dataEnum.ts b/packages/umi-serializer-data-view/src/dataEnum.ts deleted file mode 100644 index 1f44eb50..00000000 --- a/packages/umi-serializer-data-view/src/dataEnum.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - DataEnum, - DataEnumToSerializerTuple, - DataEnumSerializerOptions, - Serializer, - mergeBytes, -} from '@metaplex-foundation/umi'; -import { maxSerializerSizes } from './maxSerializerSizes'; -import { - DataViewSerializerError, - DeserializingEmptyBufferError, -} from './errors'; -import { u8 } from './numbers'; -import { sumSerializerSizes } from './sumSerializerSizes'; - -export function dataEnum( - variants: DataEnumToSerializerTuple, - options: DataEnumSerializerOptions = {} -): Serializer { - const prefix = options.size ?? u8(); - const fieldDescriptions = variants - .map( - ([name, serializer]) => - `${String(name)}${serializer ? `: ${serializer.description}` : ''}` - ) - .join(', '); - const allVariantHaveTheSameFixedSize = variants.every( - (one, i, all) => one[1].fixedSize === all[0][1].fixedSize - ); - const fixedVariantSize = allVariantHaveTheSameFixedSize - ? variants[0][1].fixedSize - : null; - const maxVariantSize = maxSerializerSizes( - variants.map(([, field]) => field.maxSize) - ); - return { - description: - options.description ?? - `dataEnum(${fieldDescriptions}; ${prefix.description})`, - fixedSize: - variants.length === 0 - ? prefix.fixedSize - : sumSerializerSizes([prefix.fixedSize, fixedVariantSize]), - maxSize: - variants.length === 0 - ? prefix.maxSize - : sumSerializerSizes([prefix.maxSize, maxVariantSize]), - serialize: (variant: T) => { - const discriminator = variants.findIndex( - ([key]) => variant.__kind === key - ); - if (discriminator < 0) { - throw new DataViewSerializerError( - `Invalid data enum variant. Got "${variant.__kind}", expected one of ` + - `[${variants.map(([key]) => key).join(', ')}]` - ); - } - const variantPrefix = prefix.serialize(discriminator); - const variantSerializer = variants[discriminator][1]; - const variantBytes = variantSerializer.serialize(variant as any); - return mergeBytes([variantPrefix, variantBytes]); - }, - deserialize: (bytes: Uint8Array, offset = 0) => { - if (bytes.slice(offset).length === 0) { - throw new DeserializingEmptyBufferError('dataEnum'); - } - const [discriminator, dOffset] = prefix.deserialize(bytes, offset); - offset = dOffset; - const variantField = variants[Number(discriminator)] ?? null; - if (!variantField) { - throw new DataViewSerializerError( - `Data enum index "${discriminator}" is out of range. ` + - `Index should be between 0 and ${variants.length - 1}.` - ); - } - const [variant, vOffset] = variantField[1].deserialize(bytes, offset); - offset = vOffset; - return [{ __kind: variantField[0], ...(variant ?? {}) } as U, offset]; - }, - }; -} diff --git a/packages/umi-serializer-data-view/src/errors.ts b/packages/umi-serializer-data-view/src/errors.ts deleted file mode 100644 index 2d7d0325..00000000 --- a/packages/umi-serializer-data-view/src/errors.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { UmiError } from '@metaplex-foundation/umi'; - -/** @category Errors */ -export class DataViewSerializerError extends UmiError { - readonly name: string = 'DataViewSerializerError'; - - constructor(message: string, cause?: Error) { - super(message, 'plugin', 'DataView Serializer', cause); - } -} - -export class OperationNotSupportedError extends DataViewSerializerError { - readonly name: string = 'OperationNotSupportedError'; - - constructor(operation: string) { - super( - `The operation [${operation}] is ` + - `not supported by the DataView Serializer.` - ); - } -} - -export class DeserializingEmptyBufferError< - TDefaultValue = undefined -> extends DataViewSerializerError { - readonly name: string = 'DeserializingEmptyBufferError'; - - readonly toleratedDefaultValue: TDefaultValue; - - constructor(serializer: string, toleratedDefaultValue?: TDefaultValue) { - super(`Serializer [${serializer}] cannot deserialize empty buffers.`); - this.toleratedDefaultValue = toleratedDefaultValue as TDefaultValue; - } -} - -export class NotEnoughBytesError extends DataViewSerializerError { - readonly name: string = 'NotEnoughBytesError'; - - constructor(serializer: string, expected: bigint | number, actual: number) { - super( - `Serializer [${serializer}] expected ${expected} bytes, got ${actual}.` - ); - } -} - -export class NumberOutOfRangeError extends DataViewSerializerError { - readonly name: string = 'NumberOutOfRangeError'; - - constructor( - serializer: string, - min: number | bigint, - max: number | bigint, - actual: number | bigint - ) { - super( - `Serializer [${serializer}] expected number to be between ${min} and ${max}, got ${actual}.` - ); - } -} diff --git a/packages/umi-serializer-data-view/src/getResolvedSize.ts b/packages/umi-serializer-data-view/src/getResolvedSize.ts deleted file mode 100644 index 054cf6a8..00000000 --- a/packages/umi-serializer-data-view/src/getResolvedSize.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ArrayLikeSerializerSize } from '@metaplex-foundation/umi'; -import { DataViewSerializerError } from './errors'; -import { sumSerializerSizes } from './sumSerializerSizes'; - -export function getResolvedSize( - size: ArrayLikeSerializerSize, - childrenSizes: (number | null)[], - bytes: Uint8Array, - offset: number -): [number | bigint, number] { - if (typeof size === 'number') { - return [size, offset]; - } - - if (typeof size === 'object') { - return size.deserialize(bytes, offset); - } - - if (size === 'remainder') { - const childrenSize = sumSerializerSizes(childrenSizes); - if (childrenSize === null) { - throw new DataViewSerializerError( - 'Serializers of "remainder" size must have fixed-size items.' - ); - } - const remainder = bytes.slice(offset).length; - if (remainder % childrenSize !== 0) { - throw new DataViewSerializerError( - `Serializers of "remainder" size must have a remainder that is a multiple of its item size. ` + - `Got ${remainder} bytes remaining and ${childrenSize} bytes per item.` - ); - } - return [remainder / childrenSize, offset]; - } - - throw new DataViewSerializerError( - `Unknown size type: ${JSON.stringify(size)}.` - ); -} diff --git a/packages/umi-serializer-data-view/src/getSizeDescription.ts b/packages/umi-serializer-data-view/src/getSizeDescription.ts deleted file mode 100644 index 3e031e9a..00000000 --- a/packages/umi-serializer-data-view/src/getSizeDescription.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ArrayLikeSerializerSize } from '@metaplex-foundation/umi'; - -export function getSizeDescription( - size: ArrayLikeSerializerSize | string -): string { - return typeof size === 'object' ? size.description : `${size}`; -} diff --git a/packages/umi-serializer-data-view/src/getSizeFromChildren.ts b/packages/umi-serializer-data-view/src/getSizeFromChildren.ts deleted file mode 100644 index 3e8fec28..00000000 --- a/packages/umi-serializer-data-view/src/getSizeFromChildren.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ArrayLikeSerializerSize } from '@metaplex-foundation/umi'; -import { sumSerializerSizes } from './sumSerializerSizes'; - -export function getSizeFromChildren( - size: ArrayLikeSerializerSize, - childrenSizes: (number | null)[] -): number | null { - if (typeof size !== 'number') return null; - if (size === 0) return 0; - const childrenSize = sumSerializerSizes(childrenSizes); - return childrenSize === null ? null : childrenSize * size; -} diff --git a/packages/umi-serializer-data-view/src/getSizePrefix.ts b/packages/umi-serializer-data-view/src/getSizePrefix.ts deleted file mode 100644 index 6a35cf8d..00000000 --- a/packages/umi-serializer-data-view/src/getSizePrefix.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ArrayLikeSerializerSize } from '@metaplex-foundation/umi'; - -export function getSizePrefix( - size: ArrayLikeSerializerSize, - realSize: number -): Uint8Array { - return typeof size === 'object' ? size.serialize(realSize) : new Uint8Array(); -} diff --git a/packages/umi-serializer-data-view/src/helpers.ts b/packages/umi-serializer-data-view/src/helpers.ts deleted file mode 100644 index 0bf00716..00000000 --- a/packages/umi-serializer-data-view/src/helpers.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Helper function to ensure that the array buffer is converted properly from a uint8array - * Source: https://stackoverflow.com/questions/37228285/uint8array-to-arraybuffer - * @param {Uint8Array} array Uint8array that's being converted into an array buffer - * @returns {ArrayBuffer} An array buffer that's necessary to construct a data view - */ -export function UInt8ArrayToBuffer(array: Uint8Array): ArrayBuffer { - return array.buffer.slice( - array.byteOffset, - array.byteLength + array.byteOffset - ); -} diff --git a/packages/umi-serializer-data-view/src/index.ts b/packages/umi-serializer-data-view/src/index.ts index c35bca6b..c081c815 100644 --- a/packages/umi-serializer-data-view/src/index.ts +++ b/packages/umi-serializer-data-view/src/index.ts @@ -1,18 +1,32 @@ -export * from './array'; +export { + array, + bool, + bytes, + dataEnum, + f32, + f64, + i128, + i16, + i32, + i64, + i8, + map, + nullable, + option, + publicKey, + scalarEnum, + set, + string, + struct, + tuple, + u128, + u16, + u32, + u64, + u8, + unit, + shortU16, +} from '@metaplex-foundation/umi/serializers'; + export * from './createDataViewSerializer'; -export * from './bool'; -export * from './bytes'; -export * from './dataEnum'; -export * from './errors'; -export * from './map'; -export * from './nullable'; -export * from './numbers'; -export * from './option'; -export * from './scalarEnum'; -export * from './set'; -export * from './string'; -export * from './struct'; -export * from './tuple'; export * from './plugin'; -export * from './pubkey'; -export * from './unit'; diff --git a/packages/umi-serializer-data-view/src/numbers.ts b/packages/umi-serializer-data-view/src/numbers.ts deleted file mode 100644 index c2ffbf2c..00000000 --- a/packages/umi-serializer-data-view/src/numbers.ts +++ /dev/null @@ -1,413 +0,0 @@ -/* eslint-disable no-bitwise */ -import { - Endian, - NumberSerializerOptions, - Serializer, - SingleByteNumberSerializerOptions, -} from '@metaplex-foundation/umi'; -import { - DeserializingEmptyBufferError, - NotEnoughBytesError, - NumberOutOfRangeError, -} from './errors'; -import { UInt8ArrayToBuffer } from './helpers'; - -const assertRange = ( - serializer: string, - min: number | bigint, - max: number | bigint, - value: number | bigint -) => { - if (value < min || value > max) { - throw new NumberOutOfRangeError(serializer, min, max, value); - } -}; - -const assertEnoughBytes = ( - serializer: string, - bytes: Uint8Array, - expected: number -) => { - if (bytes.length === 0) { - throw new DeserializingEmptyBufferError(serializer); - } - if (bytes.length < expected) { - throw new NotEnoughBytesError(serializer, expected, bytes.length); - } -}; - -export const u8 = ( - options: SingleByteNumberSerializerOptions = {} -): Serializer => ({ - description: options.description ?? 'u8', - fixedSize: 1, - maxSize: 1, - serialize(value: number): Uint8Array { - assertRange('u8', 0, Number('0xff'), value); - const buffer = new ArrayBuffer(1); - new DataView(buffer).setUint8(0, value); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [number, number] { - assertEnoughBytes('u8', bytes.slice(offset), 1); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 1)) - ); - return [view.getUint8(0), offset + 1]; - }, -}); - -export const i8 = ( - options: SingleByteNumberSerializerOptions = {} -): Serializer => ({ - description: options.description ?? 'i8', - fixedSize: 1, - maxSize: 1, - serialize(value: number): Uint8Array { - const half = Number('0x7f'); - assertRange('i8', -half - 1, half, value); - const buffer = new ArrayBuffer(1); - new DataView(buffer).setInt8(0, value); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [number, number] { - assertEnoughBytes('i8', bytes.slice(offset), 1); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 1)) - ); - return [view.getInt8(0), offset + 1]; - }, -}); - -export const u16 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'u16(le)' : 'u16(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 2, - maxSize: 2, - serialize(value: number): Uint8Array { - assertRange('u16', 0, Number('0xffff'), value); - const buffer = new ArrayBuffer(2); - new DataView(buffer).setUint16(0, value, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [number, number] { - assertEnoughBytes('u16', bytes.slice(offset), 2); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 2)) - ); - return [view.getUint16(0, littleEndian), offset + 2]; - }, - }; -}; - -export const i16 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'i16(le)' : 'i16(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 2, - maxSize: 2, - serialize(value: number): Uint8Array { - const half = Number('0x7fff'); - assertRange('i16', -half - 1, half, value); - const buffer = new ArrayBuffer(2); - new DataView(buffer).setInt16(0, value, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [number, number] { - assertEnoughBytes('i16', bytes.slice(offset), 2); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 2)) - ); - return [view.getInt16(0, littleEndian), offset + 2]; - }, - }; -}; - -/** - * Same as u16, but serialized with 1 to 3 bytes. - * - * If the value is above 0x7f, the top bit is set and the remaining - * value is stored in the next bytes. Each byte follows the same - * pattern until the 3rd byte. The 3rd byte, if needed, uses - * all 8 bits to store the last byte of the original value. - */ -export function shortU16(): Serializer { - return { - description: 'shortU16', - fixedSize: null, - maxSize: 3, - serialize: (value: number): Uint8Array => { - if (value < 0 || value > 65535) { - throw new RangeError( - `Only values in the range [0, 65535] can be serialized to shortU16. \`${value}\` given.` - ); - } - const bytes = [0]; - for (let ii = 0; ; ii += 1) { - // Shift the bits of the value over such that the next 7 bits are at the right edge. - const alignedValue = value >> (ii * 7); - if (alignedValue === 0) { - // No more bits to consume. - break; - } - // Extract those 7 bits using a mask. - const nextSevenBits = 0b1111111 & alignedValue; - bytes[ii] = nextSevenBits; - if (ii > 0) { - // Set the continuation bit of the previous slice. - bytes[ii - 1] |= 0b10000000; - } - } - return new Uint8Array(bytes); - }, - deserialize: (bytes: Uint8Array, offset = 0): [number, number] => { - let value = 0; - let byteCount = 0; - while ( - ++byteCount // eslint-disable-line no-plusplus - ) { - const byteIndex = byteCount - 1; - const currentByte = bytes[offset + byteIndex]; - const nextSevenBits = 0b1111111 & currentByte; - // Insert the next group of seven bits into the correct slot of the output value. - value |= nextSevenBits << (byteIndex * 7); - if ((currentByte & 0b10000000) === 0) { - // This byte does not have its continuation bit set. We're done. - break; - } - } - return [value, offset + byteCount]; - }, - }; -} - -export const u32 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'u32(le)' : 'u32(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 4, - maxSize: 4, - serialize(value: number): Uint8Array { - assertRange('u32', 0, Number('0xffffffff'), value); - const buffer = new ArrayBuffer(4); - new DataView(buffer).setUint32(0, value, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [number, number] { - assertEnoughBytes('u32', bytes.slice(offset), 4); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 4)) - ); - return [view.getUint32(0, littleEndian), offset + 4]; - }, - }; -}; - -export const i32 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'i32(le)' : 'i32(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 4, - maxSize: 4, - serialize(value: number): Uint8Array { - const half = Number('0x7fffffff'); - assertRange('i32', -half - 1, half, value); - const buffer = new ArrayBuffer(4); - new DataView(buffer).setInt32(0, value, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [number, number] { - assertEnoughBytes('i32', bytes.slice(offset), 4); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 4)) - ); - return [view.getInt32(0, littleEndian), offset + 4]; - }, - }; -}; - -export const u64 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'u64(le)' : 'u64(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 8, - maxSize: 8, - serialize(value: number | bigint): Uint8Array { - const valueBigInt = BigInt(value); - assertRange('u64', 0, BigInt('0xffffffffffffffff'), valueBigInt); - const buffer = new ArrayBuffer(8); - new DataView(buffer).setBigUint64(0, valueBigInt, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [bigint, number] { - assertEnoughBytes('u64', bytes.slice(offset), 8); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 8)) - ); - return [view.getBigUint64(0, littleEndian), offset + 8]; - }, - }; -}; - -export const i64 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'i64(le)' : 'i64(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 8, - maxSize: 8, - serialize(value: number | bigint): Uint8Array { - const valueBigInt = BigInt(value); - const half = BigInt('0x7fffffffffffffff'); - assertRange('i64', -half - 1n, half, valueBigInt); - const buffer = new ArrayBuffer(8); - new DataView(buffer).setBigInt64(0, valueBigInt, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [bigint, number] { - assertEnoughBytes('i64', bytes.slice(offset), 8); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 8)) - ); - return [view.getBigInt64(0, littleEndian), offset + 8]; - }, - }; -}; - -export const u128 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'u128(le)' : 'u128(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 16, - maxSize: 16, - serialize(value: number | bigint): Uint8Array { - const valueBigInt = BigInt(value); - const max = BigInt('0xffffffffffffffffffffffffffffffff'); - assertRange('u128', 0, max, valueBigInt); - const buffer = new ArrayBuffer(16); - const view = new DataView(buffer); - const leftOffset = littleEndian ? 8 : 0; - const rightOffset = littleEndian ? 0 : 8; - const rightMask = 0xffffffffffffffffn; - view.setBigUint64(leftOffset, valueBigInt >> 64n, littleEndian); - view.setBigUint64(rightOffset, valueBigInt & rightMask, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [bigint, number] { - assertEnoughBytes('u128', bytes.slice(offset), 16); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 16)) - ); - const leftOffset = littleEndian ? 8 : 0; - const rightOffset = littleEndian ? 0 : 8; - const left = view.getBigUint64(leftOffset, littleEndian); - const right = view.getBigUint64(rightOffset, littleEndian); - return [(left << 64n) + right, offset + 16]; - }, - }; -}; - -export const i128 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'i128(le)' : 'i128(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 16, - maxSize: 16, - serialize(value: number | bigint): Uint8Array { - const valueBigInt = BigInt(value); - const half = BigInt('0x7fffffffffffffffffffffffffffffff'); - assertRange('i128', -half - 1n, half, valueBigInt); - const buffer = new ArrayBuffer(16); - const view = new DataView(buffer); - const leftOffset = littleEndian ? 8 : 0; - const rightOffset = littleEndian ? 0 : 8; - const rightMask = 0xffffffffffffffffn; - view.setBigInt64(leftOffset, valueBigInt >> 64n, littleEndian); - view.setBigUint64(rightOffset, valueBigInt & rightMask, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [bigint, number] { - assertEnoughBytes('i128', bytes.slice(offset), 16); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 16)) - ); - const leftOffset = littleEndian ? 8 : 0; - const rightOffset = littleEndian ? 0 : 8; - const left = view.getBigInt64(leftOffset, littleEndian); - const right = view.getBigUint64(rightOffset, littleEndian); - return [(left << 64n) + right, offset + 16]; - }, - }; -}; - -export const f32 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'f32(le)' : 'f32(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 4, - maxSize: 4, - serialize(value: number): Uint8Array { - const buffer = new ArrayBuffer(4); - new DataView(buffer).setFloat32(0, value, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [number, number] { - assertEnoughBytes('f32', bytes.slice(offset), 4); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 4)) - ); - return [view.getFloat32(0, littleEndian), offset + 4]; - }, - }; -}; - -export const f64 = ( - options: NumberSerializerOptions = {} -): Serializer => { - const littleEndian = (options.endian ?? Endian.Little) === Endian.Little; - const defaultDescription = littleEndian ? 'f64(le)' : 'f64(be)'; - return { - description: options.description ?? defaultDescription, - fixedSize: 8, - maxSize: 8, - serialize(value: number): Uint8Array { - const buffer = new ArrayBuffer(8); - new DataView(buffer).setFloat64(0, value, littleEndian); - return new Uint8Array(buffer); - }, - deserialize(bytes, offset = 0): [number, number] { - assertEnoughBytes('f64', bytes.slice(offset), 8); - const view = new DataView( - UInt8ArrayToBuffer(bytes.slice(offset, offset + 8)) - ); - return [view.getFloat64(0, littleEndian), offset + 8]; - }, - }; -}; diff --git a/packages/umi-serializer-data-view/src/unit.ts b/packages/umi-serializer-data-view/src/unit.ts deleted file mode 100644 index 9ee3f7df..00000000 --- a/packages/umi-serializer-data-view/src/unit.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Serializer, UnitSerializerOptions } from '@metaplex-foundation/umi'; - -export function unit(options: UnitSerializerOptions = {}): Serializer { - return { - description: options.description ?? 'unit', - fixedSize: 0, - maxSize: 0, - serialize: () => new Uint8Array(), - deserialize: (_bytes: Uint8Array, offset = 0) => [undefined, offset], - }; -} diff --git a/packages/umi-serializer-data-view/test/empty.test.ts b/packages/umi-serializer-data-view/test/empty.test.ts deleted file mode 100644 index 0b008fa4..00000000 --- a/packages/umi-serializer-data-view/test/empty.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import test, { ThrowsExpectation } from 'ava'; -import { none, Serializer } from '@metaplex-foundation/umi'; -import { - createDataViewSerializer, - DeserializingEmptyBufferError, -} from '../src'; -import { u8 } from '../src/numbers'; -import { unit } from '../src/unit'; - -test('it can handle empty buffers', (t) => { - const tolerant = createDataViewSerializer(); - const intolerant = createDataViewSerializer({ tolerateEmptyBuffers: false }); - const remainder = { size: 'remainder' } as const; - const e: ThrowsExpectation = { instanceOf: DeserializingEmptyBufferError }; - const fixedError = (expectedBytes: number) => ({ - message: (m: string) => - m.includes(`Fixed serializer expected ${expectedBytes} bytes, got 0.`), - }); - const empty = (serializer: Serializer) => - serializer.deserialize(new Uint8Array())[0]; - - // Tuple. - t.throws(() => empty(tolerant.tuple([u8()])), e); - t.throws(() => empty(intolerant.tuple([u8()])), e); - t.deepEqual(empty(tolerant.tuple([])), []); - t.deepEqual(empty(intolerant.tuple([])), []); - - // Array. - t.deepEqual(empty(tolerant.array(u8())), []); - t.throws(() => empty(intolerant.array(u8())), e); - t.deepEqual(empty(tolerant.array(u8(), remainder)), []); - t.deepEqual(empty(intolerant.array(u8(), remainder)), []); - t.throws(() => empty(tolerant.array(u8(), { size: 5 })), e); - t.throws(() => empty(intolerant.array(u8(), { size: 5 })), e); - t.deepEqual(empty(tolerant.array(u8(), { size: 0 })), []); - t.deepEqual(empty(intolerant.array(u8(), { size: 0 })), []); - - // Map. - t.deepEqual(empty(tolerant.map(u8(), u8())), new Map()); - t.throws(() => empty(intolerant.map(u8(), u8())), e); - t.deepEqual(empty(tolerant.map(u8(), u8(), remainder)), new Map()); - t.deepEqual(empty(intolerant.map(u8(), u8(), remainder)), new Map()); - t.throws(() => empty(tolerant.map(u8(), u8(), { size: 5 })), e); - t.throws(() => empty(intolerant.map(u8(), u8(), { size: 5 })), e); - t.deepEqual(empty(tolerant.map(u8(), u8(), { size: 0 })), new Map()); - t.deepEqual(empty(intolerant.map(u8(), u8(), { size: 0 })), new Map()); - - // Set. - t.deepEqual(empty(tolerant.set(u8())), new Set()); - t.throws(() => empty(intolerant.set(u8())), e); - t.deepEqual(empty(tolerant.set(u8(), remainder)), new Set()); - t.deepEqual(empty(intolerant.set(u8(), remainder)), new Set()); - t.throws(() => empty(tolerant.set(u8(), { size: 5 })), e); - t.throws(() => empty(intolerant.set(u8(), { size: 5 })), e); - t.deepEqual(empty(tolerant.set(u8(), { size: 0 })), new Set()); - t.deepEqual(empty(intolerant.set(u8(), { size: 0 })), new Set()); - - // Option. - t.deepEqual(empty(tolerant.option(u8())), none()); - t.throws(() => empty(intolerant.option(u8())), e); - t.deepEqual(empty(tolerant.option(u8(), { fixed: true })), none()); - t.throws(() => empty(intolerant.option(u8(), { fixed: true })), e); - - // Nullable. - t.deepEqual(empty(tolerant.nullable(u8())), null); - t.throws(() => empty(intolerant.nullable(u8())), e); - t.deepEqual(empty(tolerant.nullable(u8(), { fixed: true })), null); - t.throws(() => empty(intolerant.nullable(u8(), { fixed: true })), e); - - // Struct. - t.throws(() => empty(tolerant.struct([['age', u8()]])), e); - t.throws(() => empty(intolerant.struct([['age', u8()]])), e); - t.deepEqual(empty(tolerant.struct([])), {}); - t.deepEqual(empty(intolerant.struct([])), {}); - - // Enum. - enum DummyEnum {} - t.throws(() => empty(tolerant.enum(DummyEnum)), e); - t.throws(() => empty(intolerant.enum(DummyEnum)), e); - - // DataEnum. - type DummyDataEnum = { __kind: 'foo' }; - t.throws(() => empty(tolerant.dataEnum([['foo', unit()]])), e); - t.throws( - () => empty(intolerant.dataEnum([['foo', unit()]])), - e - ); - - // Strings. - t.throws(() => empty(tolerant.string()), e); - t.throws(() => empty(intolerant.string()), e); - t.throws(() => empty(tolerant.string({ size: 5 })), fixedError(5)); - t.throws(() => empty(intolerant.string({ size: 5 })), fixedError(5)); - t.is(empty(tolerant.string({ size: 0 })), ''); - t.is(empty(intolerant.string({ size: 0 })), ''); - - // Bool. - t.throws(() => empty(tolerant.bool()), e); - t.throws(() => empty(intolerant.bool()), e); - - // Unit. - t.is(empty(tolerant.unit()), undefined); - t.is(empty(intolerant.unit()), undefined); - - // Numbers. - t.throws(() => empty(tolerant.u8()), e); - t.throws(() => empty(tolerant.u64()), e); - t.throws(() => empty(intolerant.u8()), e); - t.throws(() => empty(intolerant.u64()), e); - - // PublicKey. - t.throws(() => empty(tolerant.publicKey()), e); - t.throws(() => empty(intolerant.publicKey()), e); - - // Bytes. - t.deepEqual(empty(tolerant.bytes()), new Uint8Array()); - t.deepEqual(empty(intolerant.bytes()), new Uint8Array()); - t.deepEqual(empty(tolerant.bytes({ size: 'variable' })), new Uint8Array()); - t.deepEqual(empty(intolerant.bytes({ size: 'variable' })), new Uint8Array()); - t.throws(() => empty(tolerant.bytes({ size: u8() })), e); - t.throws(() => empty(intolerant.bytes({ size: u8() })), e); - t.throws(() => empty(tolerant.bytes({ size: 5 })), fixedError(5)); - t.throws(() => empty(intolerant.bytes({ size: 5 })), fixedError(5)); -}); diff --git a/packages/umi-serializer-data-view/test/numbers.test.ts b/packages/umi-serializer-data-view/test/numbers.test.ts deleted file mode 100644 index 604eb571..00000000 --- a/packages/umi-serializer-data-view/test/numbers.test.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { - Endian, - NumberSerializer, - NumberSerializerOptions, - Serializer, -} from '@metaplex-foundation/umi'; -import test, { Assertions } from 'ava'; - -import { d as baseD, s as baseS } from './_helpers'; -import { - u8, - u16, - u32, - u64, - u128, - i8, - i16, - i32, - i64, - i128, - f32, - f64, - shortU16, -} from '../src/numbers'; - -test('integer serialization', (t) => { - testIntegerSerialization(t, u8); - testIntegerSerialization(t, u16); - testIntegerSerialization(t, u32); - testIntegerSerialization(t, u64); - testIntegerSerialization(t, u128); - testIntegerSerialization(t, i8); - testIntegerSerialization(t, i16); - testIntegerSerialization(t, i32); - testIntegerSerialization(t, i64); - testIntegerSerialization(t, i128); -}); - -function testIntegerSerialization( - t: Assertions, - int: (options?: NumberSerializerOptions) => NumberSerializer -) { - t.true(typeof int().fixedSize === 'number'); - const bytes = int().fixedSize as number; - const unsigned = int().description.startsWith('u'); - const intLE = int(); - const intBE = int({ endian: Endian.Big }); - - s(t, intLE, 0n, '00'.repeat(bytes)); - s(t, intBE, 0n, '00'.repeat(bytes)); - - s(t, intLE, 1n, `01${'00'.repeat(bytes - 1)}`); - s(t, intBE, 1n, `${'00'.repeat(bytes - 1)}01`); - - s(t, intLE, 42n, `2a${'00'.repeat(bytes - 1)}`); - s(t, intBE, 42n, `${'00'.repeat(bytes - 1)}2a`); - - if (unsigned && bytes >= 2) { - const half = BigInt(`0x${'ff'.repeat(bytes / 2)}`); - s(t, intLE, half, `${'ff'.repeat(bytes / 2)}${'00'.repeat(bytes / 2)}`); - s(t, intBE, half, `${'00'.repeat(bytes / 2)}${'ff'.repeat(bytes / 2)}`); - } - - const maxBytes = BigInt(`0x${'ff'.repeat(bytes)}`); - const min = unsigned ? 0n : -(maxBytes / 2n) - 1n; - const max = unsigned ? maxBytes : maxBytes / 2n; - - if (!unsigned) { - s(t, intLE, min, `${'00'.repeat(bytes - 1)}80`); - s(t, intBE, min, `80${'00'.repeat(bytes - 1)}`); - - if (bytes >= 2) { - s(t, intLE, min + 1n, `01${'00'.repeat(bytes - 2)}80`); - s(t, intBE, min + 1n, `80${'00'.repeat(bytes - 2)}01`); - } else { - s(t, intLE, min + 1n, '81'); - s(t, intBE, min + 1n, '81'); - } - } - - const lastByte = unsigned ? 'ff' : '7f'; - s(t, intLE, max, `${'ff'.repeat(bytes - 1)}${lastByte}`); - s(t, intBE, max, `${lastByte}${'ff'.repeat(bytes - 1)}`); - - if (bytes >= 2) { - s(t, intLE, max - 1n, `fe${'ff'.repeat(bytes - 2)}${lastByte}`); - s(t, intBE, max - 1n, `${lastByte}${'ff'.repeat(bytes - 2)}fe`); - } else { - s(t, intLE, max - 1n, unsigned ? 'fe' : '7e'); - s(t, intBE, max - 1n, unsigned ? 'fe' : '7e'); - } - - sThrows(t, intLE, min - 1n); - sThrows(t, intBE, min - 1n); - sThrows(t, intLE, max + 1n); - sThrows(t, intBE, max + 1n); -} - -test('integer deserialization', (t) => { - testIntegerDeserialization(t, u8); - testIntegerDeserialization(t, u16); - testIntegerDeserialization(t, u32); - testIntegerDeserialization(t, u64); - testIntegerDeserialization(t, u128); - testIntegerDeserialization(t, i8); - testIntegerDeserialization(t, i16); - testIntegerDeserialization(t, i32); - testIntegerDeserialization(t, i64); - testIntegerDeserialization(t, i128); -}); - -function testIntegerDeserialization( - t: Assertions, - int: (options?: NumberSerializerOptions) => NumberSerializer -) { - t.true(typeof int().fixedSize === 'number'); - const bytes = int().fixedSize as number; - const unsigned = int().description.startsWith('u'); - const intLE = int(); - const intBE = int({ endian: Endian.Big }); - - d(t, intLE, '00'.repeat(bytes), 0n, bytes); - d(t, intBE, '00'.repeat(bytes), 0n, bytes); - - d(t, intLE, `01${'00'.repeat(bytes - 1)}`, 1n, bytes); - d(t, intBE, `${'00'.repeat(bytes - 1)}01`, 1n, bytes); - - d(t, intLE, [`ffffff01${'00'.repeat(bytes - 1)}`, 3], 1n, bytes + 3); - d(t, intBE, [`ffffff${'00'.repeat(bytes - 1)}01`, 3], 1n, bytes + 3); - - d(t, intLE, `2a${'00'.repeat(bytes - 1)}`, 42n, bytes); - d(t, intBE, `${'00'.repeat(bytes - 1)}2a`, 42n, bytes); - - if (unsigned && bytes >= 2) { - const half = BigInt(`0x${'ff'.repeat(bytes / 2)}`); - d(t, intLE, `${'ff'.repeat(bytes / 2)}${'00'.repeat(bytes / 2)}`, half); - d(t, intBE, `${'00'.repeat(bytes / 2)}${'ff'.repeat(bytes / 2)}`, half); - } - - const maxBytes = BigInt(`0x${'ff'.repeat(bytes)}`); - const min = unsigned ? 0n : -(maxBytes / 2n) - 1n; - const max = unsigned ? maxBytes : maxBytes / 2n; - - if (!unsigned) { - d(t, intLE, `${'00'.repeat(bytes - 1)}80`, min); - d(t, intBE, `80${'00'.repeat(bytes - 1)}`, min); - - if (bytes >= 2) { - d(t, intLE, `01${'00'.repeat(bytes - 2)}80`, min + 1n); - d(t, intBE, `80${'00'.repeat(bytes - 2)}01`, min + 1n); - } else { - d(t, intLE, '81', min + 1n); - d(t, intBE, '81', min + 1n); - } - } - - const lastByte = unsigned ? 'ff' : '7f'; - d(t, intLE, `${'ff'.repeat(bytes - 1)}${lastByte}`, max, bytes); - d(t, intBE, `${lastByte}${'ff'.repeat(bytes - 1)}`, max, bytes); - - if (bytes >= 2) { - d(t, intLE, `fe${'ff'.repeat(bytes - 2)}${lastByte}`, max - 1n); - d(t, intBE, `${lastByte}${'ff'.repeat(bytes - 2)}fe`, max - 1n); - } else { - d(t, intLE, unsigned ? 'fe' : '7e', max - 1n); - d(t, intBE, unsigned ? 'fe' : '7e', max - 1n); - } -} - -test('shortU16 serialization', (t) => { - s(t, shortU16(), 0n, '00'); - s(t, shortU16(), 127n, '7f'); - s(t, shortU16(), 128n, '8001'); - s(t, shortU16(), 16383n, 'ff7f'); - s(t, shortU16(), 16384n, '808001'); - s(t, shortU16(), 65535n, 'ffff03'); - - t.throws(() => { - shortU16().serialize(-1); - }); - t.throws(() => { - shortU16().serialize(65536); - }); - - for (let ii = 0; ii <= 0b1111111111111111; ii += 1) { - const buffer = shortU16().serialize(ii); - t.is(shortU16().deserialize(buffer)[0], ii); - } -}); - -test('description', (t) => { - // Little endian. - t.is(u8().description, 'u8'); - t.is(u16().description, 'u16(le)'); - t.is(shortU16().description, 'shortU16'); - t.is(u32().description, 'u32(le)'); - t.is(u64().description, 'u64(le)'); - t.is(u128().description, 'u128(le)'); - t.is(i8().description, 'i8'); - t.is(i16().description, 'i16(le)'); - t.is(i32().description, 'i32(le)'); - t.is(i64().description, 'i64(le)'); - t.is(i128().description, 'i128(le)'); - t.is(f32().description, 'f32(le)'); - t.is(f64().description, 'f64(le)'); - - // Big endian. - const beOptions = { endian: Endian.Big }; - t.is(u8().description, 'u8'); - t.is(u16(beOptions).description, 'u16(be)'); - t.is(u32(beOptions).description, 'u32(be)'); - t.is(u64(beOptions).description, 'u64(be)'); - t.is(u128(beOptions).description, 'u128(be)'); - t.is(i8().description, 'i8'); - t.is(i16(beOptions).description, 'i16(be)'); - t.is(i32(beOptions).description, 'i32(be)'); - t.is(i64(beOptions).description, 'i64(be)'); - t.is(i128(beOptions).description, 'i128(be)'); - t.is(f32(beOptions).description, 'f32(be)'); - t.is(f64(beOptions).description, 'f64(be)'); - - // Custom description. - t.is( - u8({ description: 'My Custom Description' }).description, - 'My Custom Description' - ); -}); - -test('sizes', (t) => { - t.is(u8().fixedSize, 1); - t.is(u8().maxSize, 1); - t.is(u16().fixedSize, 2); - t.is(u16().maxSize, 2); - t.is(shortU16().fixedSize, null); - t.is(shortU16().maxSize, 3); - t.is(u32().fixedSize, 4); - t.is(u32().maxSize, 4); - t.is(u64().fixedSize, 8); - t.is(u64().maxSize, 8); - t.is(u128().fixedSize, 16); - t.is(u128().maxSize, 16); - t.is(i8().fixedSize, 1); - t.is(i8().maxSize, 1); - t.is(i16().fixedSize, 2); - t.is(i16().maxSize, 2); - t.is(i32().fixedSize, 4); - t.is(i32().maxSize, 4); - t.is(i64().fixedSize, 8); - t.is(i64().maxSize, 8); - t.is(i128().fixedSize, 16); - t.is(i128().maxSize, 16); - t.is(f32().fixedSize, 4); - t.is(f32().maxSize, 4); - t.is(f64().fixedSize, 8); - t.is(f64().maxSize, 8); -}); - -test('float (de)serialization', (t) => { - // Zero. - baseS(t, f32(), 0, '00000000'); - baseD(t, f32(), '00000000', 0, 4); - baseD(t, f32(), ['ff00000000', 1], 0, 5); - baseS(t, f64(), 0, '0000000000000000'); - baseD(t, f64(), '0000000000000000', 0, 8); - baseD(t, f64(), ['ff0000000000000000', 1], 0, 9); - - // PI. - baseS(t, f32(), 3.141592653589793, 'db0f4940'); - baseD(t, f32(), 'db0f4940', 3.1415927410125732, 4); - baseS(t, f64(), 3.141592653589793, '182d4454fb210940'); - baseD(t, f64(), '182d4454fb210940', 3.141592653589793, 8); -}); - -function s( - t: Assertions, - serializer: NumberSerializer, - value: bigint, - expected: string -): void { - const bytes = serializer.fixedSize as number; - if (bytes <= 4) { - baseS(t, serializer as Serializer, Number(value), expected); - } else { - baseS( - t, - serializer as Serializer, - value, - expected - ); - } -} - -function sThrows( - t: Assertions, - serializer: NumberSerializer, - value: bigint -): T | undefined { - const bytes = serializer.fixedSize as number; - if (bytes <= 4) { - return t.throws(() => serializer.serialize(Number(value))); - } - return t.throws(() => - (serializer as Serializer).serialize(value) - ); -} - -function d( - t: Assertions, - serializer: NumberSerializer, - hexBytes: string | [string, number], - expected: bigint, - expectedOffset?: number -): void { - const bytes = serializer.fixedSize as number; - if (bytes <= 4) { - baseD( - t, - serializer as Serializer, - hexBytes, - Number(expected), - expectedOffset - ); - } else { - baseD( - t, - serializer as Serializer, - hexBytes, - expected, - expectedOffset - ); - } -} diff --git a/packages/umi-serializer-data-view/test/plugin.test.ts b/packages/umi-serializer-data-view/test/plugin.test.ts new file mode 100644 index 00000000..aa638446 --- /dev/null +++ b/packages/umi-serializer-data-view/test/plugin.test.ts @@ -0,0 +1,8 @@ +import { createUmi } from '@metaplex-foundation/umi'; +import test from 'ava'; +import { dataViewSerializer } from '../src'; + +test('it can use a DataView serializer', (t) => { + const umi = createUmi().use(dataViewSerializer()); + t.deepEqual(umi.serializer.u16().serialize(42), new Uint8Array([42, 0])); +}); diff --git a/packages/umi-serializers-core/README.md b/packages/umi-serializers-core/README.md new file mode 100644 index 00000000..ce729225 --- /dev/null +++ b/packages/umi-serializers-core/README.md @@ -0,0 +1,9 @@ +# umi-serializers-core + +Core types and helpers for UMI serializers + +## Installation + +```sh +npm install @metaplex-foundation/umi-serializers-core +``` diff --git a/packages/umi-serializers-core/babel.config.json b/packages/umi-serializers-core/babel.config.json new file mode 100644 index 00000000..ac08da0a --- /dev/null +++ b/packages/umi-serializers-core/babel.config.json @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} diff --git a/packages/umi-serializers-core/package.json b/packages/umi-serializers-core/package.json new file mode 100644 index 00000000..e680d1cf --- /dev/null +++ b/packages/umi-serializers-core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@metaplex-foundation/umi-serializers-core", + "version": "0.0.1", + "description": "Core types and helpers for UMI serializers", + "license": "MIT", + "sideEffects": false, + "module": "dist/esm/index.mjs", + "main": "dist/cjs/index.cjs", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, + "files": [ + "/dist/cjs", + "/dist/esm", + "/dist/types", + "/src" + ], + "scripts": { + "lint": "eslint --ext js,ts,tsx src", + "lint:fix": "eslint --fix --ext js,ts,tsx src", + "clean": "rimraf dist", + "build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c", + "test": "ava" + }, + "devDependencies": { + "@ava/typescript": "^3.0.1", + "ava": "^5.1.0" + }, + "publishConfig": { + "access": "public" + }, + "author": "Metaplex Maintainers ", + "homepage": "https://metaplex.com", + "repository": { + "url": "https://github.com/metaplex-foundation/umi.git" + }, + "typedoc": { + "entryPoint": "./src/index.ts", + "readmeFile": "./README.md", + "displayName": "umi-serializers-core" + }, + "ava": { + "typescript": { + "compile": false, + "rewritePaths": { + "src/": "dist/test/src/", + "test/": "dist/test/test/" + } + } + } +} diff --git a/packages/umi-serializers-core/rollup.config.js b/packages/umi-serializers-core/rollup.config.js new file mode 100644 index 00000000..ba38fd10 --- /dev/null +++ b/packages/umi-serializers-core/rollup.config.js @@ -0,0 +1,16 @@ +import { createConfigs } from '../../rollup.config'; +import pkg from './package.json'; + +export default createConfigs({ + pkg, + builds: [ + { + dir: 'dist/esm', + format: 'es', + }, + { + dir: 'dist/cjs', + format: 'cjs', + }, + ], +}); diff --git a/packages/umi-serializers-core/src/bytes.ts b/packages/umi-serializers-core/src/bytes.ts new file mode 100644 index 00000000..559e8dc8 --- /dev/null +++ b/packages/umi-serializers-core/src/bytes.ts @@ -0,0 +1,35 @@ +/** + * Concatenates an array of `Uint8Array`s into a single `Uint8Array`. + * @category Utils + */ +export const mergeBytes = (bytesArr: Uint8Array[]): Uint8Array => { + const totalLength = bytesArr.reduce((total, arr) => total + arr.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + bytesArr.forEach((arr) => { + result.set(arr, offset); + offset += arr.length; + }); + return result; +}; + +/** + * Pads a `Uint8Array` with zeroes to the specified length. + * If the array is longer than the specified length, it is returned as-is. + * @category Utils + */ +export const padBytes = (bytes: Uint8Array, length: number): Uint8Array => { + if (bytes.length >= length) return bytes; + const paddedBytes = new Uint8Array(length).fill(0); + paddedBytes.set(bytes); + return paddedBytes; +}; + +/** + * Fixes a `Uint8Array` to the specified length. + * If the array is longer than the specified length, it is truncated. + * If the array is shorter than the specified length, it is padded with zeroes. + * @category Utils + */ +export const fixBytes = (bytes: Uint8Array, length: number): Uint8Array => + padBytes(bytes.slice(0, length), length); diff --git a/packages/umi-serializers-core/src/common.ts b/packages/umi-serializers-core/src/common.ts new file mode 100644 index 00000000..b05199d7 --- /dev/null +++ b/packages/umi-serializers-core/src/common.ts @@ -0,0 +1,43 @@ +/** + * An object that can serialize and deserialize a value to and from a `Uint8Array`. + * It supports serializing looser types than it deserializes for convenience. + * For example, a `bigint` serializer will always deserialize to a `bigint` + * but can be used to serialize a `number`. + * + * @typeParam From - The type of the value to serialize. + * @typeParam To - The type of the deserialized value. Defaults to `From`. + * + * @category Serializers + */ +export type Serializer = { + /** A description for the serializer. */ + description: string; + /** The fixed size of the serialized value in bytes, or `null` if it is variable. */ + fixedSize: number | null; + /** The maximum size a serialized value can be in bytes, or `null` if it is variable. */ + maxSize: number | null; + /** The function that serializes a value into bytes. */ + serialize: (value: From) => Uint8Array; + /** + * The function that deserializes a value from bytes. + * It returns the deserialized value and the number of bytes read. + */ + deserialize: (buffer: Uint8Array, offset?: number) => [To, number]; +}; + +/** + * Defines common options for serializer factories. + * @category Serializers + */ +export type BaseSerializerOptions = { + /** A custom description for the serializer. */ + description?: string; +}; + +/** + * Wraps all the attributes of an object in serializers. + * @category Serializers + */ +export type WrapInSerializer = { + [P in keyof T]: Serializer; +}; diff --git a/packages/umi-serializers-core/src/errors.ts b/packages/umi-serializers-core/src/errors.ts new file mode 100644 index 00000000..97a8f1fd --- /dev/null +++ b/packages/umi-serializers-core/src/errors.ts @@ -0,0 +1,33 @@ +/** @category Errors */ +export class DeserializingEmptyBufferError extends Error { + readonly name: string = 'DeserializingEmptyBufferError'; + + constructor(serializer: string) { + super(`Serializer [${serializer}] cannot deserialize empty buffers.`); + } +} + +/** @category Errors */ +export class NotEnoughBytesError extends Error { + readonly name: string = 'NotEnoughBytesError'; + + constructor( + serializer: string, + expected: bigint | number, + actual: bigint | number + ) { + super( + `Serializer [${serializer}] expected ${expected} bytes, got ${actual}.` + ); + } +} + +/** @category Errors */ +export class ExpectedFixedSizeSerializerError extends Error { + readonly name: string = 'ExpectedFixedSizeSerializerError'; + + constructor(message?: string) { + message ??= 'Expected a fixed-size serializer, got a variable-size one.'; + super(message); + } +} diff --git a/packages/umi-serializers-core/src/fixSerializer.ts b/packages/umi-serializers-core/src/fixSerializer.ts new file mode 100644 index 00000000..3364833e --- /dev/null +++ b/packages/umi-serializers-core/src/fixSerializer.ts @@ -0,0 +1,45 @@ +import { fixBytes } from './bytes'; +import { Serializer } from './common'; +import { NotEnoughBytesError } from './errors'; + +/** + * Creates a fixed-size serializer from a given serializer. + * + * @param serializer - The serializer to wrap into a fixed-size serializer. + * @param fixedBytes - The fixed number of bytes to read. + * @param description - A custom description for the serializer. + * + * @category Serializers + */ +export function fixSerializer( + serializer: Serializer, + fixedBytes: number, + description?: string +): Serializer { + return { + description: + description ?? `fixed(${fixedBytes}, ${serializer.description})`, + fixedSize: fixedBytes, + maxSize: fixedBytes, + serialize: (value: T) => fixBytes(serializer.serialize(value), fixedBytes), + deserialize: (buffer: Uint8Array, offset = 0) => { + // Slice the buffer to the fixed size. + buffer = buffer.slice(offset, offset + fixedBytes); + // Ensure we have enough bytes. + if (buffer.length < fixedBytes) { + throw new NotEnoughBytesError( + 'fixSerializer', + fixedBytes, + buffer.length + ); + } + // If the nested serializer is fixed-size, pad and truncate the buffer accordingly. + if (serializer.fixedSize !== null) { + buffer = fixBytes(buffer, serializer.fixedSize); + } + // Deserialize the value using the nested serializer. + const [value] = serializer.deserialize(buffer, 0); + return [value, offset + fixedBytes]; + }, + }; +} diff --git a/packages/umi-serializers-core/src/index.ts b/packages/umi-serializers-core/src/index.ts new file mode 100644 index 00000000..8277467a --- /dev/null +++ b/packages/umi-serializers-core/src/index.ts @@ -0,0 +1,6 @@ +export * from './bytes'; +export * from './common'; +export * from './errors'; +export * from './fixSerializer'; +export * from './mapSerializer'; +export * from './reverseSerializer'; diff --git a/packages/umi-serializers-core/src/mapSerializer.ts b/packages/umi-serializers-core/src/mapSerializer.ts new file mode 100644 index 00000000..48b796a6 --- /dev/null +++ b/packages/umi-serializers-core/src/mapSerializer.ts @@ -0,0 +1,43 @@ +import { Serializer } from './common'; + +/** + * Converts a serializer A to a serializer B by mapping their values. + * @category Serializers + */ +export function mapSerializer( + serializer: Serializer, + unmap: (value: NewFrom) => OldFrom +): Serializer; +export function mapSerializer< + NewFrom, + OldFrom, + NewTo extends NewFrom = NewFrom, + OldTo extends OldFrom = OldFrom +>( + serializer: Serializer, + unmap: (value: NewFrom) => OldFrom, + map: (value: OldTo, buffer: Uint8Array, offset: number) => NewTo +): Serializer; +export function mapSerializer< + NewFrom, + OldFrom, + NewTo extends NewFrom = NewFrom, + OldTo extends OldFrom = OldFrom +>( + serializer: Serializer, + unmap: (value: NewFrom) => OldFrom, + map?: (value: OldTo, buffer: Uint8Array, offset: number) => NewTo +): Serializer { + return { + description: serializer.description, + fixedSize: serializer.fixedSize, + maxSize: serializer.maxSize, + serialize: (value: NewFrom) => serializer.serialize(unmap(value)), + deserialize: (buffer: Uint8Array, offset = 0) => { + const [value, length] = serializer.deserialize(buffer, offset); + return map + ? [map(value, buffer, offset), length] + : [value as any, length]; + }, + }; +} diff --git a/packages/umi-serializers-core/src/reverseSerializer.ts b/packages/umi-serializers-core/src/reverseSerializer.ts new file mode 100644 index 00000000..1163af73 --- /dev/null +++ b/packages/umi-serializers-core/src/reverseSerializer.ts @@ -0,0 +1,30 @@ +import { mergeBytes } from './bytes'; +import { Serializer } from './common'; +import { ExpectedFixedSizeSerializerError } from './errors'; + +/** + * Reverses the bytes of a fixed-size serializer. + * @category Serializers + */ +export function reverseSerializer( + serializer: Serializer +): Serializer { + if (serializer.fixedSize === null) { + throw new ExpectedFixedSizeSerializerError( + 'Cannot reverse a serializer of variable size.' + ); + } + return { + ...serializer, + serialize: (value: T) => serializer.serialize(value).reverse(), + deserialize: (bytes: Uint8Array, offset = 0) => { + const fixedSize = serializer.fixedSize as number; + const newBytes = mergeBytes([ + bytes.slice(0, offset), + bytes.slice(offset, offset + fixedSize).reverse(), + bytes.slice(offset + fixedSize), + ]); + return serializer.deserialize(newBytes, offset); + }, + }; +} diff --git a/packages/umi-serializers-core/test/_setup.ts b/packages/umi-serializers-core/test/_setup.ts new file mode 100644 index 00000000..06778da1 --- /dev/null +++ b/packages/umi-serializers-core/test/_setup.ts @@ -0,0 +1,37 @@ +import { Serializer } from '../src'; + +export const base16: Serializer = { + description: 'base16', + fixedSize: null, + maxSize: null, + serialize(value: string) { + const lowercaseValue = value.toLowerCase(); + if (!lowercaseValue.match(/^[0123456789abcdef]*$/)) { + throw new Error('Invalid base16 string'); + } + const matches = lowercaseValue.match(/.{1,2}/g); + return Uint8Array.from( + matches ? matches.map((byte: string) => parseInt(byte, 16)) : [] + ); + }, + deserialize(buffer, offset = 0) { + const value = buffer + .slice(offset) + .reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); + return [value, buffer.length]; + }, +}; + +export const utf8: Serializer = { + description: 'utf8', + fixedSize: null, + maxSize: null, + serialize(value: string) { + return new TextEncoder().encode(value); + }, + deserialize(buffer, offset = 0) { + const value = new TextDecoder().decode(buffer.slice(offset)); + // eslint-disable-next-line no-control-regex + return [value.replace(/\u0000/g, ''), buffer.length]; + }, +}; diff --git a/packages/umi-serializers-core/test/bytes.test.ts b/packages/umi-serializers-core/test/bytes.test.ts new file mode 100644 index 00000000..7d5424db --- /dev/null +++ b/packages/umi-serializers-core/test/bytes.test.ts @@ -0,0 +1,34 @@ +import test from 'ava'; +import { fixBytes, mergeBytes, padBytes } from '../src'; + +test('it can merge multiple arrays of bytes together', async (t) => { + t.deepEqual( + mergeBytes([ + new Uint8Array([1, 2, 3]), + new Uint8Array([4, 5]), + new Uint8Array([6, 7, 8, 9]), + ]), + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]) + ); +}); + +test('it can pad an array of bytes to the specified length', async (t) => { + t.deepEqual( + padBytes(new Uint8Array([1, 2, 3]), 5), + new Uint8Array([1, 2, 3, 0, 0]) + ); + + t.deepEqual( + padBytes(new Uint8Array([1, 2, 3]), 2), + new Uint8Array([1, 2, 3]) + ); +}); + +test('it can fix an array of bytes to the specified length', async (t) => { + t.deepEqual( + fixBytes(new Uint8Array([1, 2, 3]), 5), + new Uint8Array([1, 2, 3, 0, 0]) + ); + + t.deepEqual(fixBytes(new Uint8Array([1, 2, 3]), 2), new Uint8Array([1, 2])); +}); diff --git a/packages/umi-serializers-core/test/common.test.ts b/packages/umi-serializers-core/test/common.test.ts new file mode 100644 index 00000000..8243a4a1 --- /dev/null +++ b/packages/umi-serializers-core/test/common.test.ts @@ -0,0 +1,37 @@ +import test from 'ava'; +import { Serializer } from '../src'; + +test('it can define serializers', async (t) => { + const mySerializer: Serializer = { + description: 'mySerializer', + fixedSize: 32, + maxSize: 32, + serialize: (value: string) => { + const buffer = new Uint8Array(32).fill(0); + const charCodes = [...value.slice(0, 32)].map((char) => + Math.min(char.charCodeAt(0), 255) + ); + buffer.set(new Uint8Array(charCodes)); + return buffer; + }, + deserialize: (buffer: Uint8Array, offset = 0) => { + const slice = buffer.slice(offset, offset + 32); + const str = [...slice] + .map((charCode) => String.fromCharCode(charCode)) + .join(''); + return [str, offset + 32]; + }, + }; + + t.is(mySerializer.description, 'mySerializer'); + t.is(mySerializer.fixedSize, 32); + t.is(mySerializer.maxSize, 32); + + const expectedBuffer = new Uint8Array(32).fill(0); + expectedBuffer.set(new Uint8Array([104, 101, 108, 108, 111])); + t.deepEqual(mySerializer.serialize('hello'), expectedBuffer); + t.deepEqual( + mySerializer.deserialize(new Uint8Array([104, 101, 108, 108, 111])), + ['hello', 32] + ); +}); diff --git a/packages/umi-serializers-core/test/fixSerializer.test.ts b/packages/umi-serializers-core/test/fixSerializer.test.ts new file mode 100644 index 00000000..e5c63bf3 --- /dev/null +++ b/packages/umi-serializers-core/test/fixSerializer.test.ts @@ -0,0 +1,66 @@ +import test from 'ava'; +import { Serializer, fixSerializer } from '../src'; +import { base16, utf8 } from './_setup'; + +test('it can fix a serializer to a given amount of bytes', (t) => { + const b = (s: string) => base16.serialize(s); + const s = (size: number) => fixSerializer(utf8, size); + + // Description matches the fixed definition. + t.is(fixSerializer(utf8, 42).description, 'fixed(42, utf8)'); + + // Description can be overridden. + t.is(fixSerializer(utf8, 42, 'my fixed').description, 'my fixed'); + + // Fixed and max sizes. + t.is(fixSerializer(utf8, 12).fixedSize, 12); + t.is(fixSerializer(utf8, 12).maxSize, 12); + t.is(fixSerializer(utf8, 42).fixedSize, 42); + t.is(fixSerializer(utf8, 42).maxSize, 42); + + // Buffer size === fixed size. + t.deepEqual(s(12).serialize('Hello world!'), b('48656c6c6f20776f726c6421')); + t.deepEqual(s(12).deserialize(b('48656c6c6f20776f726c6421')), [ + 'Hello world!', + 12, + ]); + + // Buffer size > fixed size => truncated. + t.deepEqual(s(5).serialize('Hello world!'), b('48656c6c6f')); + t.deepEqual(s(5).deserialize(b('48656c6c6f20776f726c6421')), ['Hello', 5]); + + // Buffer size < fixed size => padded. + t.deepEqual(s(12).serialize('Hello'), b('48656c6c6f00000000000000')); + t.deepEqual(s(12).deserialize(b('48656c6c6f00000000000000')), ['Hello', 12]); + t.throws(() => s(12).deserialize(b('48656c6c6f')), { + message: (m) => + m.includes('Serializer [fixSerializer] expected 12 bytes, got 5.'), + }); +}); + +test('it can fix a serializer that requires a minimum amount of bytes (#56)', (t) => { + // Given a mock `u32` serializer that ensures the buffer is 4 bytes long. + const u32: Serializer = { + description: 'u32', + fixedSize: 4, + maxSize: 4, + serialize: (value: number) => new Uint8Array([value, 0, 0, 0]), + deserialize(bytes, offset = 0): [number, number] { + if (bytes.slice(offset).length < offset + 4) { + throw new Error('Not enough bytes to deserialize a u32.'); + } + return [bytes.slice(offset)[0], offset + 4]; + }, + }; + + // When we synthesize a `u24` from that `u32` using `fixSerializer`. + const u24 = fixSerializer(u32, 3); + + // Then we can serialize a `u24`. + const buf = u24.serialize(42); + t.deepEqual(buf, new Uint8Array([42, 0, 0])); + + // And we can deserialize it back. + const hydrated = u24.deserialize(buf); + t.deepEqual(hydrated, [42, 3]); +}); diff --git a/packages/umi/test/Serializer.test.ts b/packages/umi-serializers-core/test/mapSerializer.test.ts similarity index 56% rename from packages/umi/test/Serializer.test.ts rename to packages/umi-serializers-core/test/mapSerializer.test.ts index d0e1b024..53850f6d 100644 --- a/packages/umi/test/Serializer.test.ts +++ b/packages/umi-serializers-core/test/mapSerializer.test.ts @@ -1,12 +1,5 @@ import test from 'ava'; -import { - base16, - mapSerializer, - Serializer, - reverseSerializer, - fixSerializer, - utf8, -} from '../src'; +import { Serializer, mapSerializer } from '../src'; const numberSerializer: Serializer = { description: 'number', @@ -159,100 +152,3 @@ test('it can loosen a tuple serializer', (t) => { const bufferC = mappedSerializer.serialize([42, 'Hello world']); t.deepEqual(mappedSerializer.deserialize(bufferC)[0], [42, 'xxxxxxxxxxx']); }); - -test('it can fix a serializer to a given amount of bytes', (t) => { - const b = (s: string) => base16.serialize(s); - const s = (size: number) => fixSerializer(utf8, size); - - // Description matches the fixed definition. - t.is(fixSerializer(utf8, 42).description, 'fixed(42, utf8)'); - - // Description can be overridden. - t.is(fixSerializer(utf8, 42, 'my fixed').description, 'my fixed'); - - // Fixed and max sizes. - t.is(fixSerializer(utf8, 12).fixedSize, 12); - t.is(fixSerializer(utf8, 12).maxSize, 12); - t.is(fixSerializer(utf8, 42).fixedSize, 42); - t.is(fixSerializer(utf8, 42).maxSize, 42); - - // Buffer size === fixed size. - t.deepEqual(s(12).serialize('Hello world!'), b('48656c6c6f20776f726c6421')); - t.deepEqual(s(12).deserialize(b('48656c6c6f20776f726c6421')), [ - 'Hello world!', - 12, - ]); - - // Buffer size > fixed size => truncated. - t.deepEqual(s(5).serialize('Hello world!'), b('48656c6c6f')); - t.deepEqual(s(5).deserialize(b('48656c6c6f20776f726c6421')), ['Hello', 5]); - - // Buffer size < fixed size => padded. - t.deepEqual(s(12).serialize('Hello'), b('48656c6c6f00000000000000')); - t.deepEqual(s(12).deserialize(b('48656c6c6f00000000000000')), ['Hello', 12]); - t.throws(() => s(12).deserialize(b('48656c6c6f')), { - message: (m) => m.includes('Fixed serializer expected 12 bytes, got 5.'), - }); -}); - -test('it can fix a serializer that requires a minimum amount of bytes (#56)', (t) => { - // Given a mock `u32` serializer that ensures the buffer is 4 bytes long. - const u32: Serializer = { - description: 'u32', - fixedSize: 4, - maxSize: 4, - serialize: (value: number) => new Uint8Array([value, 0, 0, 0]), - deserialize(bytes, offset = 0): [number, number] { - if (bytes.slice(offset).length < offset + 4) { - throw new Error('Not enough bytes to deserialize a u32.'); - } - return [bytes.slice(offset)[0], offset + 4]; - }, - }; - - // When we synthesize a `u24` from that `u32` using `fixSerializer`. - const u24 = fixSerializer(u32, 3); - - // Then we can serialize a `u24`. - const buf = u24.serialize(42); - t.deepEqual(buf, new Uint8Array([42, 0, 0])); - - // And we can deserialize it back. - const hydrated = u24.deserialize(buf); - t.deepEqual(hydrated, [42, 3]); -}); - -test('it can reverse the bytes of a fixed-size serializer', (t) => { - const b = (s: string) => base16.serialize(s); - const s = (size: number) => reverseSerializer(fixSerializer(base16, size)); - - // Serialize. - t.deepEqual(s(1).serialize('00'), b('00')); - t.deepEqual(s(2).serialize('00ff'), b('ff00')); - t.deepEqual(s(2).serialize('ff00'), b('00ff')); - t.deepEqual(s(4).serialize('00000001'), b('01000000')); - t.deepEqual(s(4).serialize('01000000'), b('00000001')); - t.deepEqual(s(8).serialize('0000000000000001'), b('0100000000000000')); - t.deepEqual(s(8).serialize('0100000000000000'), b('0000000000000001')); - t.deepEqual( - s(32).serialize(`01${'00'.repeat(31)}`), - b(`${'00'.repeat(31)}01`) - ); - t.deepEqual( - s(32).serialize(`${'00'.repeat(31)}01`), - b(`01${'00'.repeat(31)}`) - ); - - // Deserialize. - t.deepEqual(s(2).deserialize(b('ff00')), ['00ff', 2]); - t.deepEqual(s(2).deserialize(b('00ff')), ['ff00', 2]); - t.deepEqual(s(4).deserialize(b('00000001')), ['01000000', 4]); - t.deepEqual(s(4).deserialize(b('01000000')), ['00000001', 4]); - t.deepEqual(s(4).deserialize(b('aaaa01000000bbbb'), 2), ['00000001', 6]); - t.deepEqual(s(4).deserialize(b('aaaa00000001bbbb'), 2), ['01000000', 6]); - - // Variable-size serializer. - t.throws(() => reverseSerializer(base16), { - message: (m) => m.includes('Cannot reverse a serializer of variable size'), - }); -}); diff --git a/packages/umi-serializers-core/test/reverseSerializer.test.ts b/packages/umi-serializers-core/test/reverseSerializer.test.ts new file mode 100644 index 00000000..94364790 --- /dev/null +++ b/packages/umi-serializers-core/test/reverseSerializer.test.ts @@ -0,0 +1,38 @@ +import test from 'ava'; +import { fixSerializer, reverseSerializer } from '../src'; +import { base16 } from './_setup'; + +test('it can reverse the bytes of a fixed-size serializer', (t) => { + const b = (s: string) => base16.serialize(s); + const s = (size: number) => reverseSerializer(fixSerializer(base16, size)); + + // Serialize. + t.deepEqual(s(1).serialize('00'), b('00')); + t.deepEqual(s(2).serialize('00ff'), b('ff00')); + t.deepEqual(s(2).serialize('ff00'), b('00ff')); + t.deepEqual(s(4).serialize('00000001'), b('01000000')); + t.deepEqual(s(4).serialize('01000000'), b('00000001')); + t.deepEqual(s(8).serialize('0000000000000001'), b('0100000000000000')); + t.deepEqual(s(8).serialize('0100000000000000'), b('0000000000000001')); + t.deepEqual( + s(32).serialize(`01${'00'.repeat(31)}`), + b(`${'00'.repeat(31)}01`) + ); + t.deepEqual( + s(32).serialize(`${'00'.repeat(31)}01`), + b(`01${'00'.repeat(31)}`) + ); + + // Deserialize. + t.deepEqual(s(2).deserialize(b('ff00')), ['00ff', 2]); + t.deepEqual(s(2).deserialize(b('00ff')), ['ff00', 2]); + t.deepEqual(s(4).deserialize(b('00000001')), ['01000000', 4]); + t.deepEqual(s(4).deserialize(b('01000000')), ['00000001', 4]); + t.deepEqual(s(4).deserialize(b('aaaa01000000bbbb'), 2), ['00000001', 6]); + t.deepEqual(s(4).deserialize(b('aaaa00000001bbbb'), 2), ['01000000', 6]); + + // Variable-size serializer. + t.throws(() => reverseSerializer(base16), { + message: (m) => m.includes('Cannot reverse a serializer of variable size'), + }); +}); diff --git a/packages/umi-serializers-core/test/tsconfig.json b/packages/umi-serializers-core/test/tsconfig.json new file mode 100644 index 00000000..2390f598 --- /dev/null +++ b/packages/umi-serializers-core/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./**/*"], + "compilerOptions": { + "module": "commonjs", + "outDir": "../dist/test", + "declarationDir": null, + "declaration": false, + "emitDeclarationOnly": false + } +} diff --git a/packages/umi-serializers-core/tsconfig.json b/packages/umi-serializers-core/tsconfig.json new file mode 100644 index 00000000..89a681d0 --- /dev/null +++ b/packages/umi-serializers-core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist/esm", + "declarationDir": "dist/types" + } +} diff --git a/packages/umi-serializers-encodings/README.md b/packages/umi-serializers-encodings/README.md new file mode 100644 index 00000000..0085df91 --- /dev/null +++ b/packages/umi-serializers-encodings/README.md @@ -0,0 +1,9 @@ +# umi-serializers-encodings + +A set of serializers that encode and decode strings + +## Installation + +```sh +npm install @metaplex-foundation/umi-serializers-encodings +``` diff --git a/packages/umi-serializers-encodings/babel.config.json b/packages/umi-serializers-encodings/babel.config.json new file mode 100644 index 00000000..ac08da0a --- /dev/null +++ b/packages/umi-serializers-encodings/babel.config.json @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} diff --git a/packages/umi-serializers-encodings/package.json b/packages/umi-serializers-encodings/package.json new file mode 100644 index 00000000..752fb60c --- /dev/null +++ b/packages/umi-serializers-encodings/package.json @@ -0,0 +1,58 @@ +{ + "name": "@metaplex-foundation/umi-serializers-encodings", + "version": "0.0.1", + "description": "A set of serializers that encode and decode strings", + "license": "MIT", + "sideEffects": false, + "module": "dist/esm/index.mjs", + "main": "dist/cjs/index.cjs", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, + "files": [ + "/dist/cjs", + "/dist/esm", + "/dist/types", + "/src" + ], + "scripts": { + "lint": "eslint --ext js,ts,tsx src", + "lint:fix": "eslint --fix --ext js,ts,tsx src", + "clean": "rimraf dist", + "build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c", + "test": "ava" + }, + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "workspace:^" + }, + "devDependencies": { + "@ava/typescript": "^3.0.1", + "ava": "^5.1.0" + }, + "publishConfig": { + "access": "public" + }, + "author": "Metaplex Maintainers ", + "homepage": "https://metaplex.com", + "repository": { + "url": "https://github.com/metaplex-foundation/umi.git" + }, + "typedoc": { + "entryPoint": "./src/index.ts", + "readmeFile": "./README.md", + "displayName": "umi-serializers-encodings" + }, + "ava": { + "typescript": { + "compile": false, + "rewritePaths": { + "src/": "dist/test/src/", + "test/": "dist/test/test/" + } + } + } +} diff --git a/packages/umi-serializers-encodings/rollup.config.js b/packages/umi-serializers-encodings/rollup.config.js new file mode 100644 index 00000000..ba38fd10 --- /dev/null +++ b/packages/umi-serializers-encodings/rollup.config.js @@ -0,0 +1,16 @@ +import { createConfigs } from '../../rollup.config'; +import pkg from './package.json'; + +export default createConfigs({ + pkg, + builds: [ + { + dir: 'dist/esm', + format: 'es', + }, + { + dir: 'dist/cjs', + format: 'cjs', + }, + ], +}); diff --git a/packages/umi-serializers-encodings/src/base10.ts b/packages/umi-serializers-encodings/src/base10.ts new file mode 100644 index 00000000..2f86b30c --- /dev/null +++ b/packages/umi-serializers-encodings/src/base10.ts @@ -0,0 +1,8 @@ +import type { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { baseX } from './baseX'; + +/** + * A string serializer that uses base10 encoding. + * @category Serializers + */ +export const base10: Serializer = baseX('0123456789'); diff --git a/packages/umi-serializers-encodings/src/base16.ts b/packages/umi-serializers-encodings/src/base16.ts new file mode 100644 index 00000000..b00e4b85 --- /dev/null +++ b/packages/umi-serializers-encodings/src/base16.ts @@ -0,0 +1,28 @@ +import type { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { InvalidBaseStringError } from './errors'; + +/** + * A string serializer that uses base16 encoding. + * @category Serializers + */ +export const base16: Serializer = { + description: 'base16', + fixedSize: null, + maxSize: null, + serialize(value: string) { + const lowercaseValue = value.toLowerCase(); + if (!lowercaseValue.match(/^[0123456789abcdef]*$/)) { + throw new InvalidBaseStringError(value, 16); + } + const matches = lowercaseValue.match(/.{1,2}/g); + return Uint8Array.from( + matches ? matches.map((byte: string) => parseInt(byte, 16)) : [] + ); + }, + deserialize(buffer, offset = 0) { + const value = buffer + .slice(offset) + .reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); + return [value, buffer.length]; + }, +}; diff --git a/packages/umi-serializers-encodings/src/base58.ts b/packages/umi-serializers-encodings/src/base58.ts new file mode 100644 index 00000000..76ed4d5e --- /dev/null +++ b/packages/umi-serializers-encodings/src/base58.ts @@ -0,0 +1,10 @@ +import type { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { baseX } from './baseX'; + +/** + * A string serializer that uses base58 encoding. + * @category Serializers + */ +export const base58: Serializer = baseX( + '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +); diff --git a/packages/umi-serializers-encodings/src/base64.ts b/packages/umi-serializers-encodings/src/base64.ts new file mode 100644 index 00000000..17e6a2d3 --- /dev/null +++ b/packages/umi-serializers-encodings/src/base64.ts @@ -0,0 +1,28 @@ +import type { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { InvalidBaseStringError } from './errors'; + +/** + * A string serializer that uses base64 encoding. + * @category Serializers + */ +export const base64: Serializer = { + description: 'base64', + fixedSize: null, + maxSize: null, + serialize(value: string) { + try { + return new Uint8Array( + atob(value) + .split('') + .map((c) => c.charCodeAt(0)) + ); + } catch (e) { + throw new InvalidBaseStringError(value, 64, e as Error); + } + }, + deserialize(buffer, offset = 0) { + const slice = buffer.slice(offset); + const value = btoa(String.fromCharCode.apply(null, [...slice])); + return [value, buffer.length]; + }, +}; diff --git a/packages/umi-serializers-encodings/src/baseX.ts b/packages/umi-serializers-encodings/src/baseX.ts new file mode 100644 index 00000000..e064e283 --- /dev/null +++ b/packages/umi-serializers-encodings/src/baseX.ts @@ -0,0 +1,72 @@ +import type { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { InvalidBaseStringError } from './errors'; + +/** + * A string serializer that uses a custom alphabet. + * This can be used to create serializers for base58, base64, etc. + * @category Serializers + */ +export const baseX = (alphabet: string): Serializer => { + const base = alphabet.length; + const baseBigInt = BigInt(base); + return { + description: `base${base}`, + fixedSize: null, + maxSize: null, + serialize(value: string): Uint8Array { + // Check if the value is valid. + if (!value.match(new RegExp(`^[${alphabet}]*$`))) { + throw new InvalidBaseStringError(value, base); + } + if (value === '') return new Uint8Array(); + + // Handle leading zeroes. + const chars = [...value]; + let trailIndex = chars.findIndex((c) => c !== alphabet[0]); + trailIndex = trailIndex === -1 ? chars.length : trailIndex; + const leadingZeroes = Array(trailIndex).fill(0); + if (trailIndex === chars.length) return Uint8Array.from(leadingZeroes); + + // From baseX to base10. + const tailChars = chars.slice(trailIndex); + let base10Number = 0n; + let baseXPower = 1n; + for (let i = tailChars.length - 1; i >= 0; i -= 1) { + base10Number += baseXPower * BigInt(alphabet.indexOf(tailChars[i])); + baseXPower *= baseBigInt; + } + + // From base10 to bytes. + const tailBytes = []; + while (base10Number > 0n) { + tailBytes.unshift(Number(base10Number % 256n)); + base10Number /= 256n; + } + return Uint8Array.from(leadingZeroes.concat(tailBytes)); + }, + deserialize(buffer, offset = 0): [string, number] { + if (buffer.length === 0) return ['', 0]; + + // Handle leading zeroes. + const bytes = buffer.slice(offset); + let trailIndex = bytes.findIndex((n) => n !== 0); + trailIndex = trailIndex === -1 ? bytes.length : trailIndex; + const leadingZeroes = alphabet[0].repeat(trailIndex); + if (trailIndex === bytes.length) return [leadingZeroes, buffer.length]; + + // From bytes to base10. + let base10Number = bytes + .slice(trailIndex) + .reduce((sum, byte) => sum * 256n + BigInt(byte), 0n); + + // From base10 to baseX. + const tailChars = []; + while (base10Number > 0n) { + tailChars.unshift(alphabet[Number(base10Number % baseBigInt)]); + base10Number /= baseBigInt; + } + + return [leadingZeroes + tailChars.join(''), buffer.length]; + }, + }; +}; diff --git a/packages/umi-serializers-encodings/src/errors.ts b/packages/umi-serializers-encodings/src/errors.ts new file mode 100644 index 00000000..1604eb81 --- /dev/null +++ b/packages/umi-serializers-encodings/src/errors.ts @@ -0,0 +1,12 @@ +/** @category Errors */ +export class InvalidBaseStringError extends Error { + readonly name: string = 'InvalidBaseStringError'; + + readonly cause?: Error; + + constructor(value: string, base: number, cause?: Error) { + const message = `Expected a string of base ${base}, got [${value}].`; + super(message); + this.cause = cause; + } +} diff --git a/packages/umi-serializers-encodings/src/index.ts b/packages/umi-serializers-encodings/src/index.ts new file mode 100644 index 00000000..e9030c06 --- /dev/null +++ b/packages/umi-serializers-encodings/src/index.ts @@ -0,0 +1,8 @@ +export * from './base10'; +export * from './base16'; +export * from './base58'; +export * from './base64'; +export * from './baseX'; +export * from './errors'; +export * from './nullCharacters'; +export * from './utf8'; diff --git a/packages/umi/src/utils/nullCharacters.ts b/packages/umi-serializers-encodings/src/nullCharacters.ts similarity index 100% rename from packages/umi/src/utils/nullCharacters.ts rename to packages/umi-serializers-encodings/src/nullCharacters.ts diff --git a/packages/umi-serializers-encodings/src/utf8.ts b/packages/umi-serializers-encodings/src/utf8.ts new file mode 100644 index 00000000..b793dc1e --- /dev/null +++ b/packages/umi-serializers-encodings/src/utf8.ts @@ -0,0 +1,20 @@ +import type { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { removeNullCharacters } from './nullCharacters'; + +/** + * A string serializer that uses UTF-8 encoding + * using the native `TextEncoder` API. + * @category Serializers + */ +export const utf8: Serializer = { + description: 'utf8', + fixedSize: null, + maxSize: null, + serialize(value: string) { + return new TextEncoder().encode(value); + }, + deserialize(buffer, offset = 0) { + const value = new TextDecoder().decode(buffer.slice(offset)); + return [removeNullCharacters(value), buffer.length]; + }, +}; diff --git a/packages/umi-serializers-encodings/test/base10.test.ts b/packages/umi-serializers-encodings/test/base10.test.ts new file mode 100644 index 00000000..f910cc6e --- /dev/null +++ b/packages/umi-serializers-encodings/test/base10.test.ts @@ -0,0 +1,30 @@ +import test from 'ava'; +import { base10 } from '../src'; + +test('it can serialize base 10 strings', (t) => { + t.deepEqual(base10.serialize(''), new Uint8Array([])); + t.deepEqual(base10.deserialize(new Uint8Array([])), ['', 0]); + + t.deepEqual(base10.serialize('0'), new Uint8Array([0])); + t.deepEqual(base10.deserialize(new Uint8Array([0])), ['0', 1]); + + t.deepEqual(base10.serialize('000'), new Uint8Array([0, 0, 0])); + t.deepEqual(base10.deserialize(new Uint8Array([0, 0, 0])), ['000', 3]); + + t.deepEqual(base10.serialize('1'), new Uint8Array([1])); + t.deepEqual(base10.deserialize(new Uint8Array([1])), ['1', 1]); + + t.deepEqual(base10.serialize('42'), new Uint8Array([42])); + t.deepEqual(base10.deserialize(new Uint8Array([42])), ['42', 1]); + + t.deepEqual(base10.serialize('1024'), new Uint8Array([4, 0])); + t.deepEqual(base10.deserialize(new Uint8Array([4, 0])), ['1024', 2]); + + t.deepEqual(base10.serialize('65535'), new Uint8Array([255, 255])); + t.deepEqual(base10.deserialize(new Uint8Array([255, 255])), ['65535', 2]); + + t.throws(() => base10.serialize('INVALID_INPUT'), { + message: (m) => + m.includes('Expected a string of base 10, got [INVALID_INPUT].'), + }); +}); diff --git a/packages/umi-serializers-encodings/test/base16.test.ts b/packages/umi-serializers-encodings/test/base16.test.ts new file mode 100644 index 00000000..2ee17415 --- /dev/null +++ b/packages/umi-serializers-encodings/test/base16.test.ts @@ -0,0 +1,29 @@ +import test from 'ava'; +import { base16 } from '../src'; + +test('it can serialize base 16 strings', (t) => { + t.deepEqual(base16.serialize(''), new Uint8Array([])); + t.deepEqual(base16.deserialize(new Uint8Array([])), ['', 0]); + + t.deepEqual(base16.serialize('0'), new Uint8Array([0])); + t.deepEqual(base16.serialize('00'), new Uint8Array([0])); + t.deepEqual(base16.deserialize(new Uint8Array([0])), ['00', 1]); + + t.deepEqual(base16.serialize('1'), new Uint8Array([1])); + t.deepEqual(base16.serialize('01'), new Uint8Array([1])); + t.deepEqual(base16.deserialize(new Uint8Array([1])), ['01', 1]); + + t.deepEqual(base16.serialize('2a'), new Uint8Array([42])); + t.deepEqual(base16.deserialize(new Uint8Array([42])), ['2a', 1]); + + t.deepEqual(base16.serialize('0400'), new Uint8Array([4, 0])); + t.deepEqual(base16.deserialize(new Uint8Array([4, 0])), ['0400', 2]); + + t.deepEqual(base16.serialize('ffff'), new Uint8Array([255, 255])); + t.deepEqual(base16.deserialize(new Uint8Array([255, 255])), ['ffff', 2]); + + t.throws(() => base16.serialize('INVALID_INPUT'), { + message: (m) => + m.includes('Expected a string of base 16, got [INVALID_INPUT].'), + }); +}); diff --git a/packages/umi-serializers-encodings/test/base58.test.ts b/packages/umi-serializers-encodings/test/base58.test.ts new file mode 100644 index 00000000..7f4b6087 --- /dev/null +++ b/packages/umi-serializers-encodings/test/base58.test.ts @@ -0,0 +1,42 @@ +import test from 'ava'; +import { base58 } from '../src'; + +test('it can serialize base 58 strings', (t) => { + t.deepEqual(base58.serialize(''), new Uint8Array([])); + t.deepEqual(base58.deserialize(new Uint8Array([])), ['', 0]); + + t.deepEqual(base58.serialize('1'), new Uint8Array([0])); + t.deepEqual(base58.deserialize(new Uint8Array([0])), ['1', 1]); + + t.deepEqual(base58.serialize('2'), new Uint8Array([1])); + t.deepEqual(base58.deserialize(new Uint8Array([1])), ['2', 1]); + + t.deepEqual(base58.serialize('11'), new Uint8Array([0, 0])); + t.deepEqual(base58.deserialize(new Uint8Array([0, 0])), ['11', 2]); + + const zeroes32 = new Uint8Array(Array(32).fill(0)); + t.deepEqual(base58.serialize('1'.repeat(32)), zeroes32); + t.deepEqual(base58.deserialize(zeroes32), ['1'.repeat(32), 32]); + + t.deepEqual(base58.serialize('j'), new Uint8Array([42])); + t.deepEqual(base58.deserialize(new Uint8Array([42])), ['j', 1]); + + t.deepEqual(base58.serialize('Jf'), new Uint8Array([4, 0])); + t.deepEqual(base58.deserialize(new Uint8Array([4, 0])), ['Jf', 2]); + + t.deepEqual(base58.serialize('LUv'), new Uint8Array([255, 255])); + t.deepEqual(base58.deserialize(new Uint8Array([255, 255])), ['LUv', 2]); + + const pubkey = 'LorisCg1FTs89a32VSrFskYDgiRbNQzct1WxyZb7nuA'; + const bytes = new Uint8Array([ + 5, 19, 4, 94, 5, 47, 73, 25, 182, 8, 150, 61, 231, 60, 102, 110, 6, 114, + 224, 110, 40, 20, 10, 184, 65, 191, 241, 204, 131, 161, 120, 181, + ]); + t.deepEqual(base58.serialize(pubkey), bytes); + t.deepEqual(base58.deserialize(bytes), [pubkey, 32]); + + t.throws(() => base58.serialize('INVALID_INPUT'), { + message: (m) => + m.includes('Expected a string of base 58, got [INVALID_INPUT].'), + }); +}); diff --git a/packages/umi-serializers-encodings/test/base64.test.ts b/packages/umi-serializers-encodings/test/base64.test.ts new file mode 100644 index 00000000..68cc41cc --- /dev/null +++ b/packages/umi-serializers-encodings/test/base64.test.ts @@ -0,0 +1,48 @@ +import test from 'ava'; +import { base16, base64 } from '../src'; + +test('it can serialize base 64 strings', (t) => { + t.deepEqual(base64.serialize(''), new Uint8Array([])); + t.deepEqual(base64.deserialize(new Uint8Array([])), ['', 0]); + + t.deepEqual(base64.serialize('AA'), new Uint8Array([0])); + t.deepEqual(base64.serialize('AA=='), new Uint8Array([0])); + t.deepEqual(base64.deserialize(new Uint8Array([0])), ['AA==', 1]); + + t.deepEqual(base64.serialize('AQ=='), new Uint8Array([1])); + t.deepEqual(base64.deserialize(new Uint8Array([1])), ['AQ==', 1]); + + t.deepEqual(base64.serialize('Kg'), new Uint8Array([42])); + t.deepEqual(base64.deserialize(new Uint8Array([42])), ['Kg==', 1]); + + const sentence = 'TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu'; + const bytes = new Uint8Array([ + 77, 97, 110, 121, 32, 104, 97, 110, 100, 115, 32, 109, 97, 107, 101, 32, + 108, 105, 103, 104, 116, 32, 119, 111, 114, 107, 46, + ]); + t.deepEqual(base64.serialize(sentence), bytes); + t.deepEqual(base64.deserialize(bytes), [sentence, 27]); + + t.throws(() => base64.serialize('INVALID_INPUT'), { + message: (m) => + m.includes('Expected a string of base 64, got [INVALID_INPUT].'), + }); + + t.throws(() => base64.serialize('A'), { + message: (m) => m.includes('Expected a string of base 64, got [A].'), + }); + + const base64TokenData = + 'AShNrkm2joOHhfQnRCzfSbrtDUkUcJSS7PJryR4PPjsnyyIWxL0ESVFoE7QWBowtz2B/iTtUGdb2EEyKbLuN5gEAAAAAAAAAAQAAAGCtpnOhgF7t+dM8By+nG51mKI9Dgb0RtO/6xvPX1w52AgAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + const base16TokenData = + '01284dae49b68e838785f427442cdf49baed0d4914709492ecf26bc91e0f3e3b27cb2216c4bd0449516813b416068c2dcf607f893b5419d6f6104c8a6cbb8de601000000000000000100000060ada673a1805eedf9d33c072fa71b9d66288f4381bd11b4effac6f3d7d70e76020000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + + t.deepEqual( + base16.deserialize(base64.serialize(base64TokenData))[0], + base16TokenData + ); + t.deepEqual( + base64.deserialize(base16.serialize(base16TokenData))[0], + base64TokenData + ); +}); diff --git a/packages/umi-serializers-encodings/test/tsconfig.json b/packages/umi-serializers-encodings/test/tsconfig.json new file mode 100644 index 00000000..2390f598 --- /dev/null +++ b/packages/umi-serializers-encodings/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./**/*"], + "compilerOptions": { + "module": "commonjs", + "outDir": "../dist/test", + "declarationDir": null, + "declaration": false, + "emitDeclarationOnly": false + } +} diff --git a/packages/umi-serializers-encodings/test/utf8.test.ts b/packages/umi-serializers-encodings/test/utf8.test.ts new file mode 100644 index 00000000..65ebc4bb --- /dev/null +++ b/packages/umi-serializers-encodings/test/utf8.test.ts @@ -0,0 +1,22 @@ +import test from 'ava'; +import { utf8 } from '../src'; + +test('it can serialize utf8 strings', (t) => { + t.deepEqual(utf8.serialize(''), new Uint8Array([])); + t.deepEqual(utf8.deserialize(new Uint8Array([])), ['', 0]); + + t.deepEqual(utf8.serialize('0'), new Uint8Array([48])); + t.deepEqual(utf8.deserialize(new Uint8Array([48])), ['0', 1]); + + t.deepEqual(utf8.serialize('ABC'), new Uint8Array([65, 66, 67])); + t.deepEqual(utf8.deserialize(new Uint8Array([65, 66, 67])), ['ABC', 3]); + + const serializedHelloWorld = new Uint8Array([ + 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, + ]); + t.deepEqual(utf8.serialize('Hello World!'), serializedHelloWorld); + t.deepEqual(utf8.deserialize(serializedHelloWorld), ['Hello World!', 12]); + + t.deepEqual(utf8.serialize('語'), new Uint8Array([232, 170, 158])); + t.deepEqual(utf8.deserialize(new Uint8Array([232, 170, 158])), ['語', 3]); +}); diff --git a/packages/umi-serializers-encodings/tsconfig.json b/packages/umi-serializers-encodings/tsconfig.json new file mode 100644 index 00000000..89a681d0 --- /dev/null +++ b/packages/umi-serializers-encodings/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist/esm", + "declarationDir": "dist/types" + } +} diff --git a/packages/umi-serializers-numbers/README.md b/packages/umi-serializers-numbers/README.md new file mode 100644 index 00000000..f427b05e --- /dev/null +++ b/packages/umi-serializers-numbers/README.md @@ -0,0 +1,9 @@ +# umi-serializers-numbers + +A set of serializers for numbers + +## Installation + +```sh +npm install @metaplex-foundation/umi-serializers-numbers +``` diff --git a/packages/umi-serializers-numbers/babel.config.json b/packages/umi-serializers-numbers/babel.config.json new file mode 100644 index 00000000..ac08da0a --- /dev/null +++ b/packages/umi-serializers-numbers/babel.config.json @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} diff --git a/packages/umi-serializers-numbers/package.json b/packages/umi-serializers-numbers/package.json new file mode 100644 index 00000000..54974180 --- /dev/null +++ b/packages/umi-serializers-numbers/package.json @@ -0,0 +1,58 @@ +{ + "name": "@metaplex-foundation/umi-serializers-numbers", + "version": "0.0.1", + "description": "A set of serializers for numbers", + "license": "MIT", + "sideEffects": false, + "module": "dist/esm/index.mjs", + "main": "dist/cjs/index.cjs", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, + "files": [ + "/dist/cjs", + "/dist/esm", + "/dist/types", + "/src" + ], + "scripts": { + "lint": "eslint --ext js,ts,tsx src", + "lint:fix": "eslint --fix --ext js,ts,tsx src", + "clean": "rimraf dist", + "build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c", + "test": "ava" + }, + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "workspace:^" + }, + "devDependencies": { + "@ava/typescript": "^3.0.1", + "ava": "^5.1.0" + }, + "publishConfig": { + "access": "public" + }, + "author": "Metaplex Maintainers ", + "homepage": "https://metaplex.com", + "repository": { + "url": "https://github.com/metaplex-foundation/umi.git" + }, + "typedoc": { + "entryPoint": "./src/index.ts", + "readmeFile": "./README.md", + "displayName": "umi-serializers-numbers" + }, + "ava": { + "typescript": { + "compile": false, + "rewritePaths": { + "src/": "dist/test/src/", + "test/": "dist/test/test/" + } + } + } +} diff --git a/packages/umi-serializers-numbers/rollup.config.js b/packages/umi-serializers-numbers/rollup.config.js new file mode 100644 index 00000000..ba38fd10 --- /dev/null +++ b/packages/umi-serializers-numbers/rollup.config.js @@ -0,0 +1,16 @@ +import { createConfigs } from '../../rollup.config'; +import pkg from './package.json'; + +export default createConfigs({ + pkg, + builds: [ + { + dir: 'dist/esm', + format: 'es', + }, + { + dir: 'dist/cjs', + format: 'cjs', + }, + ], +}); diff --git a/packages/umi-serializers-numbers/src/common.ts b/packages/umi-serializers-numbers/src/common.ts new file mode 100644 index 00000000..3ab1d652 --- /dev/null +++ b/packages/umi-serializers-numbers/src/common.ts @@ -0,0 +1,39 @@ +import { + BaseSerializerOptions, + Serializer, +} from '@metaplex-foundation/umi-serializers-core'; + +/** + * Defines a serializer for numbers and bigints. + * @category Serializers + */ +export type NumberSerializer = + | Serializer + | Serializer; + +/** + * Defines the options for u8 and i8 serializers. + * @category Serializers + */ +export type SingleByteNumberSerializerOptions = BaseSerializerOptions; + +/** + * Defines the options for number serializers that use more than one byte. + * @category Serializers + */ +export type NumberSerializerOptions = BaseSerializerOptions & { + /** + * Whether the serializer should use little-endian or big-endian encoding. + * @defaultValue `Endian.Little` + */ + endian?: Endian; +}; + +/** + * Defines the endianness of a number serializer. + * @category Serializers + */ +export enum Endian { + Little = 'le', + Big = 'be', +} diff --git a/packages/umi-serializers-numbers/src/errors.ts b/packages/umi-serializers-numbers/src/errors.ts new file mode 100644 index 00000000..8bbfca28 --- /dev/null +++ b/packages/umi-serializers-numbers/src/errors.ts @@ -0,0 +1,15 @@ +/** @category Errors */ +export class NumberOutOfRangeError extends RangeError { + readonly name: string = 'NumberOutOfRangeError'; + + constructor( + serializer: string, + min: number | bigint, + max: number | bigint, + actual: number | bigint + ) { + super( + `Serializer [${serializer}] expected number to be between ${min} and ${max}, got ${actual}.` + ); + } +} diff --git a/packages/umi-serializers-numbers/src/f32.ts b/packages/umi-serializers-numbers/src/f32.ts new file mode 100644 index 00000000..290ab749 --- /dev/null +++ b/packages/umi-serializers-numbers/src/f32.ts @@ -0,0 +1,14 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const f32 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'f32', + size: 4, + set: (view, value, le) => view.setFloat32(0, Number(value), le), + get: (view, le) => view.getFloat32(0, le), + options, + }); diff --git a/packages/umi-serializers-numbers/src/f64.ts b/packages/umi-serializers-numbers/src/f64.ts new file mode 100644 index 00000000..04e559d2 --- /dev/null +++ b/packages/umi-serializers-numbers/src/f64.ts @@ -0,0 +1,14 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const f64 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'f64', + size: 8, + set: (view, value, le) => view.setFloat64(0, Number(value), le), + get: (view, le) => view.getFloat64(0, le), + options, + }); diff --git a/packages/umi-serializers-numbers/src/i128.ts b/packages/umi-serializers-numbers/src/i128.ts new file mode 100644 index 00000000..21e1cece --- /dev/null +++ b/packages/umi-serializers-numbers/src/i128.ts @@ -0,0 +1,31 @@ +/* eslint-disable no-bitwise */ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const i128 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'i128', + size: 16, + range: [ + -BigInt('0x7fffffffffffffffffffffffffffffff') - 1n, + BigInt('0x7fffffffffffffffffffffffffffffff'), + ], + set: (view, value, le) => { + const leftOffset = le ? 8 : 0; + const rightOffset = le ? 0 : 8; + const rightMask = 0xffffffffffffffffn; + view.setBigInt64(leftOffset, BigInt(value) >> 64n, le); + view.setBigUint64(rightOffset, BigInt(value) & rightMask, le); + }, + get: (view, le) => { + const leftOffset = le ? 8 : 0; + const rightOffset = le ? 0 : 8; + const left = view.getBigInt64(leftOffset, le); + const right = view.getBigUint64(rightOffset, le); + return (left << 64n) + right; + }, + options, + }); diff --git a/packages/umi-serializers-numbers/src/i16.ts b/packages/umi-serializers-numbers/src/i16.ts new file mode 100644 index 00000000..a1024596 --- /dev/null +++ b/packages/umi-serializers-numbers/src/i16.ts @@ -0,0 +1,15 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const i16 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'i16', + size: 2, + range: [-Number('0x7fff') - 1, Number('0x7fff')], + set: (view, value, le) => view.setInt16(0, Number(value), le), + get: (view, le) => view.getInt16(0, le), + options, + }); diff --git a/packages/umi-serializers-numbers/src/i32.ts b/packages/umi-serializers-numbers/src/i32.ts new file mode 100644 index 00000000..f5e7f5b2 --- /dev/null +++ b/packages/umi-serializers-numbers/src/i32.ts @@ -0,0 +1,15 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const i32 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'i32', + size: 4, + range: [-Number('0x7fffffff') - 1, Number('0x7fffffff')], + set: (view, value, le) => view.setInt32(0, Number(value), le), + get: (view, le) => view.getInt32(0, le), + options, + }); diff --git a/packages/umi-serializers-numbers/src/i64.ts b/packages/umi-serializers-numbers/src/i64.ts new file mode 100644 index 00000000..b16cdc6e --- /dev/null +++ b/packages/umi-serializers-numbers/src/i64.ts @@ -0,0 +1,15 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const i64 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'i64', + size: 8, + range: [-BigInt('0x7fffffffffffffff') - 1n, BigInt('0x7fffffffffffffff')], + set: (view, value, le) => view.setBigInt64(0, BigInt(value), le), + get: (view, le) => view.getBigInt64(0, le), + options, + }); diff --git a/packages/umi-serializers-numbers/src/i8.ts b/packages/umi-serializers-numbers/src/i8.ts new file mode 100644 index 00000000..1c2549fa --- /dev/null +++ b/packages/umi-serializers-numbers/src/i8.ts @@ -0,0 +1,15 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { SingleByteNumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const i8 = ( + options: SingleByteNumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'i8', + size: 1, + range: [-Number('0x7f') - 1, Number('0x7f')], + set: (view, value) => view.setInt8(0, Number(value)), + get: (view) => view.getInt8(0), + options, + }); diff --git a/packages/umi-serializers-numbers/src/index.ts b/packages/umi-serializers-numbers/src/index.ts new file mode 100644 index 00000000..6bd6e938 --- /dev/null +++ b/packages/umi-serializers-numbers/src/index.ts @@ -0,0 +1,15 @@ +export * from './common'; +export * from './errors'; +export * from './f32'; +export * from './f64'; +export * from './i8'; +export * from './i16'; +export * from './i32'; +export * from './i64'; +export * from './i128'; +export * from './u8'; +export * from './u16'; +export * from './u32'; +export * from './u64'; +export * from './u128'; +export * from './shortU16'; diff --git a/packages/umi-serializers-numbers/src/shortU16.ts b/packages/umi-serializers-numbers/src/shortU16.ts new file mode 100644 index 00000000..c96b994e --- /dev/null +++ b/packages/umi-serializers-numbers/src/shortU16.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-bitwise */ +import { + BaseSerializerOptions, + Serializer, +} from '@metaplex-foundation/umi-serializers-core'; +import { assertRange } from './utils'; + +/** + * Defines the options for the shortU16 serializer. + * @category Serializers + */ +export type ShortU16SerializerOptions = BaseSerializerOptions; + +/** + * Same as u16, but serialized with 1 to 3 bytes. + * + * If the value is above 0x7f, the top bit is set and the remaining + * value is stored in the next bytes. Each byte follows the same + * pattern until the 3rd byte. The 3rd byte, if needed, uses + * all 8 bits to store the last byte of the original value. + * + * @category Serializers + */ +export const shortU16 = ( + options: ShortU16SerializerOptions = {} +): Serializer => ({ + description: options.description ?? 'shortU16', + fixedSize: null, + maxSize: 3, + serialize: (value: number): Uint8Array => { + assertRange('shortU16', 0, 65535, value); + const bytes = [0]; + for (let ii = 0; ; ii += 1) { + // Shift the bits of the value over such that the next 7 bits are at the right edge. + const alignedValue = value >> (ii * 7); + if (alignedValue === 0) { + // No more bits to consume. + break; + } + // Extract those 7 bits using a mask. + const nextSevenBits = 0b1111111 & alignedValue; + bytes[ii] = nextSevenBits; + if (ii > 0) { + // Set the continuation bit of the previous slice. + bytes[ii - 1] |= 0b10000000; + } + } + return new Uint8Array(bytes); + }, + deserialize: (bytes: Uint8Array, offset = 0): [number, number] => { + let value = 0; + let byteCount = 0; + while ( + ++byteCount // eslint-disable-line no-plusplus + ) { + const byteIndex = byteCount - 1; + const currentByte = bytes[offset + byteIndex]; + const nextSevenBits = 0b1111111 & currentByte; + // Insert the next group of seven bits into the correct slot of the output value. + value |= nextSevenBits << (byteIndex * 7); + if ((currentByte & 0b10000000) === 0) { + // This byte does not have its continuation bit set. We're done. + break; + } + } + return [value, offset + byteCount]; + }, +}); diff --git a/packages/umi-serializers-numbers/src/u128.ts b/packages/umi-serializers-numbers/src/u128.ts new file mode 100644 index 00000000..31c639f9 --- /dev/null +++ b/packages/umi-serializers-numbers/src/u128.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-bitwise */ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const u128 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'u128', + size: 16, + range: [0, BigInt('0xffffffffffffffffffffffffffffffff')], + set: (view, value, le) => { + const leftOffset = le ? 8 : 0; + const rightOffset = le ? 0 : 8; + const rightMask = 0xffffffffffffffffn; + view.setBigUint64(leftOffset, BigInt(value) >> 64n, le); + view.setBigUint64(rightOffset, BigInt(value) & rightMask, le); + }, + get: (view, le) => { + const leftOffset = le ? 8 : 0; + const rightOffset = le ? 0 : 8; + const left = view.getBigUint64(leftOffset, le); + const right = view.getBigUint64(rightOffset, le); + return (left << 64n) + right; + }, + options, + }); diff --git a/packages/umi-serializers-numbers/src/u16.ts b/packages/umi-serializers-numbers/src/u16.ts new file mode 100644 index 00000000..65c2db85 --- /dev/null +++ b/packages/umi-serializers-numbers/src/u16.ts @@ -0,0 +1,15 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const u16 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'u16', + size: 2, + range: [0, Number('0xffff')], + set: (view, value, le) => view.setUint16(0, Number(value), le), + get: (view, le) => view.getUint16(0, le), + options, + }); diff --git a/packages/umi-serializers-numbers/src/u32.ts b/packages/umi-serializers-numbers/src/u32.ts new file mode 100644 index 00000000..02761156 --- /dev/null +++ b/packages/umi-serializers-numbers/src/u32.ts @@ -0,0 +1,15 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const u32 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'u32', + size: 4, + range: [0, Number('0xffffffff')], + set: (view, value, le) => view.setUint32(0, Number(value), le), + get: (view, le) => view.getUint32(0, le), + options, + }); diff --git a/packages/umi-serializers-numbers/src/u64.ts b/packages/umi-serializers-numbers/src/u64.ts new file mode 100644 index 00000000..96c6cfed --- /dev/null +++ b/packages/umi-serializers-numbers/src/u64.ts @@ -0,0 +1,15 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const u64 = ( + options: NumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'u64', + size: 8, + range: [0, BigInt('0xffffffffffffffff')], + set: (view, value, le) => view.setBigUint64(0, BigInt(value), le), + get: (view, le) => view.getBigUint64(0, le), + options, + }); diff --git a/packages/umi-serializers-numbers/src/u8.ts b/packages/umi-serializers-numbers/src/u8.ts new file mode 100644 index 00000000..6c3567ec --- /dev/null +++ b/packages/umi-serializers-numbers/src/u8.ts @@ -0,0 +1,15 @@ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { SingleByteNumberSerializerOptions } from './common'; +import { numberFactory } from './utils'; + +export const u8 = ( + options: SingleByteNumberSerializerOptions = {} +): Serializer => + numberFactory({ + name: 'u8', + size: 1, + range: [0, Number('0xff')], + set: (view, value) => view.setUint8(0, Number(value)), + get: (view) => view.getUint8(0), + options, + }); diff --git a/packages/umi-serializers-numbers/src/utils.ts b/packages/umi-serializers-numbers/src/utils.ts new file mode 100644 index 00000000..fcdd35c3 --- /dev/null +++ b/packages/umi-serializers-numbers/src/utils.ts @@ -0,0 +1,102 @@ +import { + DeserializingEmptyBufferError, + NotEnoughBytesError, + Serializer, +} from '@metaplex-foundation/umi-serializers-core'; +import { + Endian, + NumberSerializer, + NumberSerializerOptions, + SingleByteNumberSerializerOptions, +} from './common'; +import { NumberOutOfRangeError } from './errors'; + +export function numberFactory(input: { + name: string; + size: number; + range?: [number | bigint, number | bigint]; + set: (view: DataView, value: number | bigint, littleEndian?: boolean) => void; + get: (view: DataView, littleEndian?: boolean) => number; + options: SingleByteNumberSerializerOptions | NumberSerializerOptions; +}): Serializer; +export function numberFactory(input: { + name: string; + size: number; + range?: [number | bigint, number | bigint]; + set: (view: DataView, value: number | bigint, littleEndian?: boolean) => void; + get: (view: DataView, littleEndian?: boolean) => bigint; + options: SingleByteNumberSerializerOptions | NumberSerializerOptions; +}): Serializer; +export function numberFactory(input: { + name: string; + size: number; + range?: [number | bigint, number | bigint]; + set: (view: DataView, value: number | bigint, littleEndian?: boolean) => void; + get: (view: DataView, littleEndian?: boolean) => number | bigint; + options: SingleByteNumberSerializerOptions | NumberSerializerOptions; +}): NumberSerializer { + let littleEndian: boolean | undefined; + let defaultDescription: string = input.name; + + if (input.size > 1) { + littleEndian = + !('endian' in input.options) || input.options.endian === Endian.Little; + defaultDescription += littleEndian ? '(le)' : '(be)'; + } + + return { + description: input.options.description ?? defaultDescription, + fixedSize: input.size, + maxSize: input.size, + serialize(value: number | bigint): Uint8Array { + if (input.range) { + assertRange(input.name, input.range[0], input.range[1], value); + } + const buffer = new ArrayBuffer(input.size); + input.set(new DataView(buffer), value, littleEndian); + return new Uint8Array(buffer); + }, + deserialize(bytes, offset = 0): [number | bigint, number] { + const slice = bytes.slice(offset, offset + input.size); + assertEnoughBytes('i8', slice, input.size); + const view = toDataView(slice); + return [input.get(view, littleEndian), offset + input.size]; + }, + } as NumberSerializer; +} + +/** + * Helper function to ensure that the array buffer is converted properly from a uint8array + * Source: https://stackoverflow.com/questions/37228285/uint8array-to-arraybuffer + * @param {Uint8Array} array Uint8array that's being converted into an array buffer + * @returns {ArrayBuffer} An array buffer that's necessary to construct a data view + */ +export const toArrayBuffer = (array: Uint8Array): ArrayBuffer => + array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset); + +export const toDataView = (array: Uint8Array): DataView => + new DataView(toArrayBuffer(array)); + +export const assertRange = ( + serializer: string, + min: number | bigint, + max: number | bigint, + value: number | bigint +) => { + if (value < min || value > max) { + throw new NumberOutOfRangeError(serializer, min, max, value); + } +}; + +export const assertEnoughBytes = ( + serializer: string, + bytes: Uint8Array, + expected: number +) => { + if (bytes.length === 0) { + throw new DeserializingEmptyBufferError(serializer); + } + if (bytes.length < expected) { + throw new NotEnoughBytesError(serializer, expected, bytes.length); + } +}; diff --git a/packages/umi-serializers-numbers/test/_setup.ts b/packages/umi-serializers-numbers/test/_setup.ts new file mode 100644 index 00000000..a4b86b0a --- /dev/null +++ b/packages/umi-serializers-numbers/test/_setup.ts @@ -0,0 +1,59 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { Serializer } from '@metaplex-foundation/umi-serializers-core'; +import { Assertions } from 'ava'; + +export const assertValid = ( + t: Assertions, + serializer: Serializer, + number: T, + bytes: string, + deserializedNumber?: T +): void => { + // Serialize. + const actualBytes = serializer.serialize(number); + const [actualBytesBase16] = base16.deserialize(actualBytes); + t.is(actualBytesBase16, bytes); + // Deserialize. + const deserialization = serializer.deserialize(actualBytes); + t.is(deserialization[0], deserializedNumber ?? number); + t.is(deserialization[1], actualBytes.length); + // Deserialize with offset. + const deserializationWithOffset = serializer.deserialize( + base16.serialize(`ffffff${bytes}`), + 3 + ); + t.is(deserializationWithOffset[0], deserializedNumber ?? number); + t.is(deserializationWithOffset[1], actualBytes.length + 3); +}; + +export const assertRangeError = ( + t: Assertions, + serializer: Serializer, + number: T +): void => { + t.throws(() => serializer.serialize(number), { + name: 'NumberOutOfRangeError', + }); +}; + +export const base16: Serializer = { + description: 'base16', + fixedSize: null, + maxSize: null, + serialize(value: string) { + const lowercaseValue = value.toLowerCase(); + if (!lowercaseValue.match(/^[0123456789abcdef]*$/)) { + throw new Error('Invalid base16 string'); + } + const matches = lowercaseValue.match(/.{1,2}/g); + return Uint8Array.from( + matches ? matches.map((byte: string) => parseInt(byte, 16)) : [] + ); + }, + deserialize(buffer, offset = 0) { + const value = buffer + .slice(offset) + .reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); + return [value, buffer.length]; + }, +}; diff --git a/packages/umi-serializers-numbers/test/f32.test.ts b/packages/umi-serializers-numbers/test/f32.test.ts new file mode 100644 index 00000000..120fc365 --- /dev/null +++ b/packages/umi-serializers-numbers/test/f32.test.ts @@ -0,0 +1,39 @@ +import test from 'ava'; +import { Endian, f32 } from '../src'; +import { assertValid } from './_setup'; + +const APPROX_PI = 3.1415927410125732; + +test('serialization', (t) => { + const f32LE = f32(); + const f32BE = f32({ endian: Endian.Big }); + + assertValid(t, f32LE, 0, '00000000'); + assertValid(t, f32BE, 0, '00000000'); + + assertValid(t, f32LE, 1, '0000803f'); + assertValid(t, f32BE, 1, '3f800000'); + assertValid(t, f32LE, 42, '00002842'); + assertValid(t, f32BE, 42, '42280000'); + assertValid(t, f32LE, Math.PI, 'db0f4940', APPROX_PI); + assertValid(t, f32BE, Math.PI, '40490fdb', APPROX_PI); + + assertValid(t, f32LE, -1, '000080bf'); + assertValid(t, f32BE, -1, 'bf800000'); + assertValid(t, f32LE, -42, '000028c2'); + assertValid(t, f32BE, -42, 'c2280000'); + assertValid(t, f32LE, -Math.PI, 'db0f49c0', -APPROX_PI); + assertValid(t, f32BE, -Math.PI, 'c0490fdb', -APPROX_PI); +}); + +test('description', (t) => { + t.is(f32().description, 'f32(le)'); + t.is(f32({ endian: Endian.Little }).description, 'f32(le)'); + t.is(f32({ endian: Endian.Big }).description, 'f32(be)'); + t.is(f32({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(f32().fixedSize, 4); + t.is(f32().maxSize, 4); +}); diff --git a/packages/umi-serializers-numbers/test/f64.test.ts b/packages/umi-serializers-numbers/test/f64.test.ts new file mode 100644 index 00000000..0493efc7 --- /dev/null +++ b/packages/umi-serializers-numbers/test/f64.test.ts @@ -0,0 +1,39 @@ +import test from 'ava'; +import { Endian, f64 } from '../src'; +import { assertValid } from './_setup'; + +const APPROX_PI = 3.141592653589793; + +test('serialization', (t) => { + const f64LE = f64(); + const f64BE = f64({ endian: Endian.Big }); + + assertValid(t, f64LE, 0, '0000000000000000'); + assertValid(t, f64BE, 0, '0000000000000000'); + + assertValid(t, f64LE, 1, '000000000000f03f'); + assertValid(t, f64BE, 1, '3ff0000000000000'); + assertValid(t, f64LE, 42, '0000000000004540'); + assertValid(t, f64BE, 42, '4045000000000000'); + assertValid(t, f64LE, Math.PI, '182d4454fb210940', APPROX_PI); + assertValid(t, f64BE, Math.PI, '400921fb54442d18', APPROX_PI); + + assertValid(t, f64LE, -1, '000000000000f0bf'); + assertValid(t, f64BE, -1, 'bff0000000000000'); + assertValid(t, f64LE, -42, '00000000000045c0'); + assertValid(t, f64BE, -42, 'c045000000000000'); + assertValid(t, f64LE, -Math.PI, '182d4454fb2109c0', -APPROX_PI); + assertValid(t, f64BE, -Math.PI, 'c00921fb54442d18', -APPROX_PI); +}); + +test('description', (t) => { + t.is(f64().description, 'f64(le)'); + t.is(f64({ endian: Endian.Little }).description, 'f64(le)'); + t.is(f64({ endian: Endian.Big }).description, 'f64(be)'); + t.is(f64({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(f64().fixedSize, 8); + t.is(f64().maxSize, 8); +}); diff --git a/packages/umi-serializers-numbers/test/i128.test.ts b/packages/umi-serializers-numbers/test/i128.test.ts new file mode 100644 index 00000000..7719a4c7 --- /dev/null +++ b/packages/umi-serializers-numbers/test/i128.test.ts @@ -0,0 +1,52 @@ +import test from 'ava'; +import { Endian, i128 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = -BigInt('0x7fffffffffffffffffffffffffffffff') - 1n; +const MAX = BigInt('0x7fffffffffffffffffffffffffffffff'); + +test('serialization', (t) => { + const i128LE = i128(); + const i128BE = i128({ endian: Endian.Big }); + + assertValid(t, i128LE, 0n, '00000000000000000000000000000000'); + assertValid(t, i128BE, 0n, '00000000000000000000000000000000'); + assertValid(t, i128LE, 1n, '01000000000000000000000000000000'); + assertValid(t, i128BE, 1n, '00000000000000000000000000000001'); + assertValid(t, i128LE, 42n, '2a000000000000000000000000000000'); + assertValid(t, i128BE, 42n, '0000000000000000000000000000002a'); + assertValid(t, i128LE, -1n, 'ffffffffffffffffffffffffffffffff'); + assertValid(t, i128BE, -1n, 'ffffffffffffffffffffffffffffffff'); + assertValid(t, i128LE, -42n, 'd6ffffffffffffffffffffffffffffff'); + assertValid(t, i128BE, -42n, 'ffffffffffffffffffffffffffffffd6'); + + // Pre-boundaries. + assertValid(t, i128LE, MIN + 1n, '01000000000000000000000000000080'); + assertValid(t, i128BE, MIN + 1n, '80000000000000000000000000000001'); + assertValid(t, i128LE, MAX - 1n, 'feffffffffffffffffffffffffffff7f'); + assertValid(t, i128BE, MAX - 1n, '7ffffffffffffffffffffffffffffffe'); + + // Boundaries. + assertValid(t, i128LE, MIN, '00000000000000000000000000000080'); + assertValid(t, i128BE, MIN, '80000000000000000000000000000000'); + assertValid(t, i128LE, MAX, 'ffffffffffffffffffffffffffffff7f'); + assertValid(t, i128BE, MAX, '7fffffffffffffffffffffffffffffff'); + + // Out of range. + assertRangeError(t, i128LE, MIN - 1n); + assertRangeError(t, i128BE, MIN - 1n); + assertRangeError(t, i128LE, MAX + 1n); + assertRangeError(t, i128BE, MAX + 1n); +}); + +test('description', (t) => { + t.is(i128().description, 'i128(le)'); + t.is(i128({ endian: Endian.Little }).description, 'i128(le)'); + t.is(i128({ endian: Endian.Big }).description, 'i128(be)'); + t.is(i128({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(i128().fixedSize, 16); + t.is(i128().maxSize, 16); +}); diff --git a/packages/umi-serializers-numbers/test/i16.test.ts b/packages/umi-serializers-numbers/test/i16.test.ts new file mode 100644 index 00000000..52175613 --- /dev/null +++ b/packages/umi-serializers-numbers/test/i16.test.ts @@ -0,0 +1,52 @@ +import test from 'ava'; +import { Endian, i16 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = -Number('0x7fff') - 1; +const MAX = Number('0x7fff'); + +test('serialization', (t) => { + const i16LE = i16(); + const i16BE = i16({ endian: Endian.Big }); + + assertValid(t, i16LE, 0, '0000'); + assertValid(t, i16BE, 0, '0000'); + assertValid(t, i16LE, 1, '0100'); + assertValid(t, i16BE, 1, '0001'); + assertValid(t, i16LE, 42, '2a00'); + assertValid(t, i16BE, 42, '002a'); + assertValid(t, i16LE, -1, 'ffff'); + assertValid(t, i16BE, -1, 'ffff'); + assertValid(t, i16LE, -42, 'd6ff'); + assertValid(t, i16BE, -42, 'ffd6'); + + // Pre-boundaries. + assertValid(t, i16LE, MIN + 1, '0180'); + assertValid(t, i16BE, MIN + 1, '8001'); + assertValid(t, i16LE, MAX - 1, 'fe7f'); + assertValid(t, i16BE, MAX - 1, '7ffe'); + + // Boundaries. + assertValid(t, i16LE, MIN, '0080'); + assertValid(t, i16BE, MIN, '8000'); + assertValid(t, i16LE, MAX, 'ff7f'); + assertValid(t, i16BE, MAX, '7fff'); + + // Out of range. + assertRangeError(t, i16LE, MIN - 1); + assertRangeError(t, i16BE, MIN - 1); + assertRangeError(t, i16LE, MAX + 1); + assertRangeError(t, i16BE, MAX + 1); +}); + +test('description', (t) => { + t.is(i16().description, 'i16(le)'); + t.is(i16({ endian: Endian.Little }).description, 'i16(le)'); + t.is(i16({ endian: Endian.Big }).description, 'i16(be)'); + t.is(i16({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(i16().fixedSize, 2); + t.is(i16().maxSize, 2); +}); diff --git a/packages/umi-serializers-numbers/test/i32.test.ts b/packages/umi-serializers-numbers/test/i32.test.ts new file mode 100644 index 00000000..1bea030b --- /dev/null +++ b/packages/umi-serializers-numbers/test/i32.test.ts @@ -0,0 +1,52 @@ +import test from 'ava'; +import { Endian, i32 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = -Number('0x7fffffff') - 1; +const MAX = Number('0x7fffffff'); + +test('serialization', (t) => { + const i32LE = i32(); + const i32BE = i32({ endian: Endian.Big }); + + assertValid(t, i32LE, 0, '00000000'); + assertValid(t, i32BE, 0, '00000000'); + assertValid(t, i32LE, 1, '01000000'); + assertValid(t, i32BE, 1, '00000001'); + assertValid(t, i32LE, 42, '2a000000'); + assertValid(t, i32BE, 42, '0000002a'); + assertValid(t, i32LE, -1, 'ffffffff'); + assertValid(t, i32BE, -1, 'ffffffff'); + assertValid(t, i32LE, -42, 'd6ffffff'); + assertValid(t, i32BE, -42, 'ffffffd6'); + + // Pre-boundaries. + assertValid(t, i32LE, MIN + 1, '01000080'); + assertValid(t, i32BE, MIN + 1, '80000001'); + assertValid(t, i32LE, MAX - 1, 'feffff7f'); + assertValid(t, i32BE, MAX - 1, '7ffffffe'); + + // Boundaries. + assertValid(t, i32LE, MIN, '00000080'); + assertValid(t, i32BE, MIN, '80000000'); + assertValid(t, i32LE, MAX, 'ffffff7f'); + assertValid(t, i32BE, MAX, '7fffffff'); + + // Out of range. + assertRangeError(t, i32LE, MIN - 1); + assertRangeError(t, i32BE, MIN - 1); + assertRangeError(t, i32LE, MAX + 1); + assertRangeError(t, i32BE, MAX + 1); +}); + +test('description', (t) => { + t.is(i32().description, 'i32(le)'); + t.is(i32({ endian: Endian.Little }).description, 'i32(le)'); + t.is(i32({ endian: Endian.Big }).description, 'i32(be)'); + t.is(i32({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(i32().fixedSize, 4); + t.is(i32().maxSize, 4); +}); diff --git a/packages/umi-serializers-numbers/test/i64.test.ts b/packages/umi-serializers-numbers/test/i64.test.ts new file mode 100644 index 00000000..3273b010 --- /dev/null +++ b/packages/umi-serializers-numbers/test/i64.test.ts @@ -0,0 +1,52 @@ +import test from 'ava'; +import { Endian, i64 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = -BigInt('0x7fffffffffffffff') - 1n; +const MAX = BigInt('0x7fffffffffffffff'); + +test('serialization', (t) => { + const i64LE = i64(); + const i64BE = i64({ endian: Endian.Big }); + + assertValid(t, i64LE, 0n, '0000000000000000'); + assertValid(t, i64BE, 0n, '0000000000000000'); + assertValid(t, i64LE, 1n, '0100000000000000'); + assertValid(t, i64BE, 1n, '0000000000000001'); + assertValid(t, i64LE, 42n, '2a00000000000000'); + assertValid(t, i64BE, 42n, '000000000000002a'); + assertValid(t, i64LE, -1n, 'ffffffffffffffff'); + assertValid(t, i64BE, -1n, 'ffffffffffffffff'); + assertValid(t, i64LE, -42n, 'd6ffffffffffffff'); + assertValid(t, i64BE, -42n, 'ffffffffffffffd6'); + + // Pre-boundaries. + assertValid(t, i64LE, MIN + 1n, '0100000000000080'); + assertValid(t, i64BE, MIN + 1n, '8000000000000001'); + assertValid(t, i64LE, MAX - 1n, 'feffffffffffff7f'); + assertValid(t, i64BE, MAX - 1n, '7ffffffffffffffe'); + + // Boundaries. + assertValid(t, i64LE, MIN, '0000000000000080'); + assertValid(t, i64BE, MIN, '8000000000000000'); + assertValid(t, i64LE, MAX, 'ffffffffffffff7f'); + assertValid(t, i64BE, MAX, '7fffffffffffffff'); + + // Out of range. + assertRangeError(t, i64LE, MIN - 1n); + assertRangeError(t, i64BE, MIN - 1n); + assertRangeError(t, i64LE, MAX + 1n); + assertRangeError(t, i64BE, MAX + 1n); +}); + +test('description', (t) => { + t.is(i64().description, 'i64(le)'); + t.is(i64({ endian: Endian.Little }).description, 'i64(le)'); + t.is(i64({ endian: Endian.Big }).description, 'i64(be)'); + t.is(i64({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(i64().fixedSize, 8); + t.is(i64().maxSize, 8); +}); diff --git a/packages/umi-serializers-numbers/test/i8.test.ts b/packages/umi-serializers-numbers/test/i8.test.ts new file mode 100644 index 00000000..32758922 --- /dev/null +++ b/packages/umi-serializers-numbers/test/i8.test.ts @@ -0,0 +1,36 @@ +import test from 'ava'; +import { i8 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = -Number('0x7f') - 1; +const MAX = Number('0x7f'); + +test('serialization', (t) => { + assertValid(t, i8(), 0, '00'); + assertValid(t, i8(), 1, '01'); + assertValid(t, i8(), 42, '2a'); + assertValid(t, i8(), -1, 'ff'); + assertValid(t, i8(), -42, 'd6'); + + // Pre-boundaries. + assertValid(t, i8(), MIN + 1, '81'); + assertValid(t, i8(), MAX - 1, '7e'); + + // Boundaries. + assertValid(t, i8(), MIN, '80'); + assertValid(t, i8(), MAX, '7f'); + + // Out of range. + assertRangeError(t, i8(), MIN - 1); + assertRangeError(t, i8(), MAX + 1); +}); + +test('description', (t) => { + t.is(i8().description, 'i8'); + t.is(i8({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(i8().fixedSize, 1); + t.is(i8().maxSize, 1); +}); diff --git a/packages/umi-serializers-numbers/test/shortU16.test.ts b/packages/umi-serializers-numbers/test/shortU16.test.ts new file mode 100644 index 00000000..1c03ccb8 --- /dev/null +++ b/packages/umi-serializers-numbers/test/shortU16.test.ts @@ -0,0 +1,45 @@ +import test from 'ava'; +import { shortU16 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = 0; +const MAX = 65535; + +test('serialization', (t) => { + assertValid(t, shortU16(), 0, '00'); + assertValid(t, shortU16(), 1, '01'); + assertValid(t, shortU16(), 42, '2a'); + assertValid(t, shortU16(), 127, '7f'); + assertValid(t, shortU16(), 128, '8001'); + assertValid(t, shortU16(), 16383, 'ff7f'); + assertValid(t, shortU16(), 16384, '808001'); + + // Pre-boundaries. + assertValid(t, shortU16(), MIN + 1, '01'); + assertValid(t, shortU16(), MAX - 1, 'feff03'); + + // Boundaries. + assertValid(t, shortU16(), MIN, '00'); + assertValid(t, shortU16(), MAX, 'ffff03'); + + // Out of range. + assertRangeError(t, shortU16(), MIN - 1); + assertRangeError(t, shortU16(), MAX + 1); + + // Assert re-serialization. + const serializer = shortU16(); + for (let i = 0; i <= 0b1111111111111111; i += 1) { + const buffer = serializer.serialize(i); + t.is(serializer.deserialize(buffer)[0], i); + } +}); + +test('description', (t) => { + t.is(shortU16().description, 'shortU16'); + t.is(shortU16({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(shortU16().fixedSize, null); + t.is(shortU16().maxSize, 3); +}); diff --git a/packages/umi-serializers-numbers/test/tsconfig.json b/packages/umi-serializers-numbers/test/tsconfig.json new file mode 100644 index 00000000..2390f598 --- /dev/null +++ b/packages/umi-serializers-numbers/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./**/*"], + "compilerOptions": { + "module": "commonjs", + "outDir": "../dist/test", + "declarationDir": null, + "declaration": false, + "emitDeclarationOnly": false + } +} diff --git a/packages/umi-serializers-numbers/test/u128.test.ts b/packages/umi-serializers-numbers/test/u128.test.ts new file mode 100644 index 00000000..5bdffdd8 --- /dev/null +++ b/packages/umi-serializers-numbers/test/u128.test.ts @@ -0,0 +1,51 @@ +import test from 'ava'; +import { Endian, u128 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = 0n; +const MAX = BigInt('0xffffffffffffffffffffffffffffffff'); +const HALF = BigInt('0xffffffffffffffff'); + +test('serialization', (t) => { + const u128LE = u128(); + const u128BE = u128({ endian: Endian.Big }); + + assertValid(t, u128LE, 1n, '01000000000000000000000000000000'); + assertValid(t, u128BE, 1n, '00000000000000000000000000000001'); + assertValid(t, u128LE, 42n, '2a000000000000000000000000000000'); + assertValid(t, u128BE, 42n, '0000000000000000000000000000002a'); + + // Half bytes. + assertValid(t, u128LE, HALF, 'ffffffffffffffff0000000000000000'); + assertValid(t, u128BE, HALF, '0000000000000000ffffffffffffffff'); + + // Pre-boundaries. + assertValid(t, u128LE, MIN + 1n, '01000000000000000000000000000000'); + assertValid(t, u128BE, MIN + 1n, '00000000000000000000000000000001'); + assertValid(t, u128LE, MAX - 1n, 'feffffffffffffffffffffffffffffff'); + assertValid(t, u128BE, MAX - 1n, 'fffffffffffffffffffffffffffffffe'); + + // Boundaries. + assertValid(t, u128LE, MIN, '00000000000000000000000000000000'); + assertValid(t, u128BE, MIN, '00000000000000000000000000000000'); + assertValid(t, u128LE, MAX, 'ffffffffffffffffffffffffffffffff'); + assertValid(t, u128BE, MAX, 'ffffffffffffffffffffffffffffffff'); + + // Out of range. + assertRangeError(t, u128LE, MIN - 1n); + assertRangeError(t, u128BE, MIN - 1n); + assertRangeError(t, u128LE, MAX + 1n); + assertRangeError(t, u128BE, MAX + 1n); +}); + +test('description', (t) => { + t.is(u128().description, 'u128(le)'); + t.is(u128({ endian: Endian.Little }).description, 'u128(le)'); + t.is(u128({ endian: Endian.Big }).description, 'u128(be)'); + t.is(u128({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(u128().fixedSize, 16); + t.is(u128().maxSize, 16); +}); diff --git a/packages/umi-serializers-numbers/test/u16.test.ts b/packages/umi-serializers-numbers/test/u16.test.ts new file mode 100644 index 00000000..ad062579 --- /dev/null +++ b/packages/umi-serializers-numbers/test/u16.test.ts @@ -0,0 +1,51 @@ +import test from 'ava'; +import { Endian, u16 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = 0; +const MAX = Number('0xffff'); +const HALF = Number('0xff'); + +test('serialization', (t) => { + const u16LE = u16(); + const u16BE = u16({ endian: Endian.Big }); + + assertValid(t, u16LE, 1, '0100'); + assertValid(t, u16BE, 1, '0001'); + assertValid(t, u16LE, 42, '2a00'); + assertValid(t, u16BE, 42, '002a'); + + // Half bytes. + assertValid(t, u16LE, HALF, 'ff00'); + assertValid(t, u16BE, HALF, '00ff'); + + // Pre-boundaries. + assertValid(t, u16LE, MIN + 1, '0100'); + assertValid(t, u16BE, MIN + 1, '0001'); + assertValid(t, u16LE, MAX - 1, 'feff'); + assertValid(t, u16BE, MAX - 1, 'fffe'); + + // Boundaries. + assertValid(t, u16LE, MIN, '0000'); + assertValid(t, u16BE, MIN, '0000'); + assertValid(t, u16LE, MAX, 'ffff'); + assertValid(t, u16BE, MAX, 'ffff'); + + // Out of range. + assertRangeError(t, u16LE, MIN - 1); + assertRangeError(t, u16BE, MIN - 1); + assertRangeError(t, u16LE, MAX + 1); + assertRangeError(t, u16BE, MAX + 1); +}); + +test('description', (t) => { + t.is(u16().description, 'u16(le)'); + t.is(u16({ endian: Endian.Little }).description, 'u16(le)'); + t.is(u16({ endian: Endian.Big }).description, 'u16(be)'); + t.is(u16({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(u16().fixedSize, 2); + t.is(u16().maxSize, 2); +}); diff --git a/packages/umi-serializers-numbers/test/u32.test.ts b/packages/umi-serializers-numbers/test/u32.test.ts new file mode 100644 index 00000000..6964bb94 --- /dev/null +++ b/packages/umi-serializers-numbers/test/u32.test.ts @@ -0,0 +1,51 @@ +import test from 'ava'; +import { Endian, u32 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = 0; +const MAX = Number('0xffffffff'); +const HALF = Number('0xffff'); + +test('serialization', (t) => { + const u32LE = u32(); + const u32BE = u32({ endian: Endian.Big }); + + assertValid(t, u32LE, 1, '01000000'); + assertValid(t, u32BE, 1, '00000001'); + assertValid(t, u32LE, 42, '2a000000'); + assertValid(t, u32BE, 42, '0000002a'); + + // Half bytes. + assertValid(t, u32LE, HALF, 'ffff0000'); + assertValid(t, u32BE, HALF, '0000ffff'); + + // Pre-boundaries. + assertValid(t, u32LE, MIN + 1, '01000000'); + assertValid(t, u32BE, MIN + 1, '00000001'); + assertValid(t, u32LE, MAX - 1, 'feffffff'); + assertValid(t, u32BE, MAX - 1, 'fffffffe'); + + // Boundaries. + assertValid(t, u32LE, MIN, '00000000'); + assertValid(t, u32BE, MIN, '00000000'); + assertValid(t, u32LE, MAX, 'ffffffff'); + assertValid(t, u32BE, MAX, 'ffffffff'); + + // Out of range. + assertRangeError(t, u32LE, MIN - 1); + assertRangeError(t, u32BE, MIN - 1); + assertRangeError(t, u32LE, MAX + 1); + assertRangeError(t, u32BE, MAX + 1); +}); + +test('description', (t) => { + t.is(u32().description, 'u32(le)'); + t.is(u32({ endian: Endian.Little }).description, 'u32(le)'); + t.is(u32({ endian: Endian.Big }).description, 'u32(be)'); + t.is(u32({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(u32().fixedSize, 4); + t.is(u32().maxSize, 4); +}); diff --git a/packages/umi-serializers-numbers/test/u64.test.ts b/packages/umi-serializers-numbers/test/u64.test.ts new file mode 100644 index 00000000..c829229b --- /dev/null +++ b/packages/umi-serializers-numbers/test/u64.test.ts @@ -0,0 +1,51 @@ +import test from 'ava'; +import { Endian, u64 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = 0n; +const MAX = BigInt('0xffffffffffffffff'); +const HALF = BigInt('0xffffffff'); + +test('serialization', (t) => { + const u64LE = u64(); + const u64BE = u64({ endian: Endian.Big }); + + assertValid(t, u64LE, 1n, '0100000000000000'); + assertValid(t, u64BE, 1n, '0000000000000001'); + assertValid(t, u64LE, 42n, '2a00000000000000'); + assertValid(t, u64BE, 42n, '000000000000002a'); + + // Half bytes. + assertValid(t, u64LE, HALF, 'ffffffff00000000'); + assertValid(t, u64BE, HALF, '00000000ffffffff'); + + // Pre-boundaries. + assertValid(t, u64LE, MIN + 1n, '0100000000000000'); + assertValid(t, u64BE, MIN + 1n, '0000000000000001'); + assertValid(t, u64LE, MAX - 1n, 'feffffffffffffff'); + assertValid(t, u64BE, MAX - 1n, 'fffffffffffffffe'); + + // Boundaries. + assertValid(t, u64LE, MIN, '0000000000000000'); + assertValid(t, u64BE, MIN, '0000000000000000'); + assertValid(t, u64LE, MAX, 'ffffffffffffffff'); + assertValid(t, u64BE, MAX, 'ffffffffffffffff'); + + // Out of range. + assertRangeError(t, u64LE, MIN - 1n); + assertRangeError(t, u64BE, MIN - 1n); + assertRangeError(t, u64LE, MAX + 1n); + assertRangeError(t, u64BE, MAX + 1n); +}); + +test('description', (t) => { + t.is(u64().description, 'u64(le)'); + t.is(u64({ endian: Endian.Little }).description, 'u64(le)'); + t.is(u64({ endian: Endian.Big }).description, 'u64(be)'); + t.is(u64({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(u64().fixedSize, 8); + t.is(u64().maxSize, 8); +}); diff --git a/packages/umi-serializers-numbers/test/u8.test.ts b/packages/umi-serializers-numbers/test/u8.test.ts new file mode 100644 index 00000000..26d29949 --- /dev/null +++ b/packages/umi-serializers-numbers/test/u8.test.ts @@ -0,0 +1,33 @@ +import test from 'ava'; +import { u8 } from '../src'; +import { assertRangeError, assertValid } from './_setup'; + +const MIN = 0; +const MAX = Number('0xff'); + +test('serialization', (t) => { + assertValid(t, u8(), 1, '01'); + assertValid(t, u8(), 42, '2a'); + + // Pre-boundaries. + assertValid(t, u8(), MIN + 1, '01'); + assertValid(t, u8(), MAX - 1, 'fe'); + + // Boundaries. + assertValid(t, u8(), MIN, '00'); + assertValid(t, u8(), MAX, 'ff'); + + // Out of range. + assertRangeError(t, u8(), MIN - 1); + assertRangeError(t, u8(), MAX + 1); +}); + +test('description', (t) => { + t.is(u8().description, 'u8'); + t.is(u8({ description: 'custom' }).description, 'custom'); +}); + +test('sizes', (t) => { + t.is(u8().fixedSize, 1); + t.is(u8().maxSize, 1); +}); diff --git a/packages/umi-serializers-numbers/tsconfig.json b/packages/umi-serializers-numbers/tsconfig.json new file mode 100644 index 00000000..89a681d0 --- /dev/null +++ b/packages/umi-serializers-numbers/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist/esm", + "declarationDir": "dist/types" + } +} diff --git a/packages/umi-serializers/README.md b/packages/umi-serializers/README.md new file mode 100644 index 00000000..f56ad0c1 --- /dev/null +++ b/packages/umi-serializers/README.md @@ -0,0 +1,9 @@ +# umi-serializers + +A comprehensive set of serializers for the Umi framework + +## Installation + +```sh +npm install @metaplex-foundation/umi-serializers +``` diff --git a/packages/umi-serializers/babel.config.json b/packages/umi-serializers/babel.config.json new file mode 100644 index 00000000..ac08da0a --- /dev/null +++ b/packages/umi-serializers/babel.config.json @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} diff --git a/packages/umi-serializers/package.json b/packages/umi-serializers/package.json new file mode 100644 index 00000000..03a70f16 --- /dev/null +++ b/packages/umi-serializers/package.json @@ -0,0 +1,62 @@ +{ + "name": "@metaplex-foundation/umi-serializers", + "version": "0.0.1", + "description": "A comprehensive set of serializers for the Umi framework", + "license": "MIT", + "sideEffects": false, + "module": "dist/esm/index.mjs", + "main": "dist/cjs/index.cjs", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, + "files": [ + "/dist/cjs", + "/dist/esm", + "/dist/types", + "/src" + ], + "scripts": { + "lint": "eslint --ext js,ts,tsx src", + "lint:fix": "eslint --fix --ext js,ts,tsx src", + "clean": "rimraf dist", + "build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c", + "test": "ava" + }, + "dependencies": { + "@metaplex-foundation/umi-options": "workspace:^", + "@metaplex-foundation/umi-public-keys": "workspace:^", + "@metaplex-foundation/umi-serializers-core": "workspace:^", + "@metaplex-foundation/umi-serializers-numbers": "workspace:^", + "@metaplex-foundation/umi-serializers-encodings": "workspace:^" + }, + "devDependencies": { + "@ava/typescript": "^3.0.1", + "ava": "^5.1.0" + }, + "publishConfig": { + "access": "public" + }, + "author": "Metaplex Maintainers ", + "homepage": "https://metaplex.com", + "repository": { + "url": "https://github.com/metaplex-foundation/umi.git" + }, + "typedoc": { + "entryPoint": "./src/index.ts", + "readmeFile": "./README.md", + "displayName": "umi-serializers" + }, + "ava": { + "typescript": { + "compile": false, + "rewritePaths": { + "src/": "dist/test/src/", + "test/": "dist/test/test/" + } + } + } +} diff --git a/packages/umi-serializers/rollup.config.js b/packages/umi-serializers/rollup.config.js new file mode 100644 index 00000000..ba38fd10 --- /dev/null +++ b/packages/umi-serializers/rollup.config.js @@ -0,0 +1,16 @@ +import { createConfigs } from '../../rollup.config'; +import pkg from './package.json'; + +export default createConfigs({ + pkg, + builds: [ + { + dir: 'dist/esm', + format: 'es', + }, + { + dir: 'dist/cjs', + format: 'cjs', + }, + ], +}); diff --git a/packages/umi-serializer-data-view/src/array.ts b/packages/umi-serializers/src/array.ts similarity index 58% rename from packages/umi-serializer-data-view/src/array.ts rename to packages/umi-serializers/src/array.ts index e78cccd9..91399ff7 100644 --- a/packages/umi-serializer-data-view/src/array.ts +++ b/packages/umi-serializers/src/array.ts @@ -1,25 +1,45 @@ import { - ArraySerializerOptions, - mergeBytes, + BaseSerializerOptions, + ExpectedFixedSizeSerializerError, Serializer, -} from '@metaplex-foundation/umi'; + mergeBytes, +} from '@metaplex-foundation/umi-serializers-core'; +import { u32 } from '@metaplex-foundation/umi-serializers-numbers'; +import { ArrayLikeSerializerSize } from './arrayLikeSerializerSize'; +import { InvalidNumberOfItemsError } from './errors'; import { - DataViewSerializerError, - DeserializingEmptyBufferError, -} from './errors'; -import { getSizeDescription } from './getSizeDescription'; -import { getResolvedSize } from './getResolvedSize'; -import { getSizeFromChildren } from './getSizeFromChildren'; -import { getSizePrefix } from './getSizePrefix'; -import { u32 } from './numbers'; + getResolvedSize, + getSizeDescription, + getSizeFromChildren, + getSizePrefix, +} from './utils'; + +/** + * Defines the options for array serializers. + * @category Serializers + */ +export type ArraySerializerOptions = BaseSerializerOptions & { + /** + * The size of the array. + * @defaultValue `u32()` + */ + size?: ArrayLikeSerializerSize; +}; +/** + * Creates a serializer for an array of items. + * + * @param item - The serializer to use for the array's items. + * @param options - A set of options for the serializer. + * @category Serializers + */ export function array( item: Serializer, options: ArraySerializerOptions = {} ): Serializer { const size = options.size ?? u32(); if (size === 'remainder' && item.fixedSize === null) { - throw new DataViewSerializerError( + throw new ExpectedFixedSizeSerializerError( 'Serializers of "remainder" size must have fixed-size items.' ); } @@ -31,9 +51,7 @@ export function array( maxSize: getSizeFromChildren(size, [item.maxSize]), serialize: (value: T[]) => { if (typeof size === 'number' && value.length !== size) { - throw new DataViewSerializerError( - `Expected array to have ${size} items but got ${value.length}.` - ); + throw new InvalidNumberOfItemsError('array', size, value.length); } return mergeBytes([ getSizePrefix(size, value.length), @@ -42,7 +60,7 @@ export function array( }, deserialize: (bytes: Uint8Array, offset = 0) => { if (typeof size === 'object' && bytes.slice(offset).length === 0) { - throw new DeserializingEmptyBufferError('array', []); + return [[], offset]; } const [resolvedSize, newOffset] = getResolvedSize( size, diff --git a/packages/umi-serializers/src/arrayLikeSerializerSize.ts b/packages/umi-serializers/src/arrayLikeSerializerSize.ts new file mode 100644 index 00000000..af6e2ddf --- /dev/null +++ b/packages/umi-serializers/src/arrayLikeSerializerSize.ts @@ -0,0 +1,16 @@ +import { NumberSerializer } from '@metaplex-foundation/umi-serializers-numbers'; + +/** + * Represents all the size options for array-like serializers + * — i.e. `array`, `map` and `set`. + * + * It can be one of the following: + * - a {@link NumberSerializer} that prefixes its content with its size. + * - a fixed number of items. + * - or `'remainder'` to infer the number of items by dividing + * the rest of the buffer by the fixed size of its item. + * Note that this option is only available for fixed-size items. + * + * @category Serializers + */ +export type ArrayLikeSerializerSize = NumberSerializer | number | 'remainder'; diff --git a/packages/umi-serializers/src/bitArray.ts b/packages/umi-serializers/src/bitArray.ts new file mode 100644 index 00000000..8a84a5c4 --- /dev/null +++ b/packages/umi-serializers/src/bitArray.ts @@ -0,0 +1,82 @@ +/* eslint-disable no-bitwise */ +import { + BaseSerializerOptions, + NotEnoughBytesError, + Serializer, +} from '@metaplex-foundation/umi-serializers-core'; + +/** + * Defines the options for bitArray serializers. + * @category Serializers + */ +export type BitArraySerializerOptions = BaseSerializerOptions & { + /** + * Whether to read the bits in reverse order. + * @defaultValue `false` + */ + backward?: boolean; +}; + +/** + * An array of boolean serializer that + * converts booleans to bits and vice versa. + * + * @param size - The amount of bytes to use for the bit array. + * @param options - A set of options for the serializer. + * @category Serializers + */ +export const bitArray = ( + size: number, + options: BitArraySerializerOptions | boolean = {} +): Serializer => { + const parsedOptions: BitArraySerializerOptions = + typeof options === 'boolean' ? { backward: options } : options; + const backward = parsedOptions.backward ?? false; + const backwardSuffix = backward ? '; backward' : ''; + return { + description: + parsedOptions.description ?? `bitArray(${size}${backwardSuffix})`, + fixedSize: size, + maxSize: size, + serialize(value: boolean[]) { + const bytes: number[] = []; + + for (let i = 0; i < size; i += 1) { + let byte = 0; + for (let j = 0; j < 8; j += 1) { + const feature = Number(value[i * 8 + j] ?? 0); + byte |= feature << (backward ? j : 7 - j); + } + if (backward) { + bytes.unshift(byte); + } else { + bytes.push(byte); + } + } + + return new Uint8Array(bytes); + }, + deserialize(bytes, offset = 0) { + const booleans: boolean[] = []; + let slice = bytes.slice(offset, offset + size); + slice = backward ? slice.reverse() : slice; + if (slice.length !== size) { + throw new NotEnoughBytesError('bitArray', size, slice.length); + } + + slice.forEach((byte) => { + for (let i = 0; i < 8; i += 1) { + if (backward) { + booleans.push(Boolean(byte & 1)); + byte >>= 1; + } else { + booleans.push(Boolean(byte & 0b1000_0000)); + byte <<= 1; + } + } + }); + + return [booleans, offset + size]; + }, + }; +}; diff --git a/packages/umi-serializer-data-view/src/bool.ts b/packages/umi-serializers/src/bool.ts similarity index 53% rename from packages/umi-serializer-data-view/src/bool.ts rename to packages/umi-serializers/src/bool.ts index cc914b9a..a2a156da 100644 --- a/packages/umi-serializer-data-view/src/bool.ts +++ b/packages/umi-serializers/src/bool.ts @@ -1,14 +1,36 @@ -import { BoolSerializerOptions, Serializer } from '@metaplex-foundation/umi'; import { - DataViewSerializerError, + BaseSerializerOptions, DeserializingEmptyBufferError, -} from './errors'; -import { u8 } from './numbers'; + ExpectedFixedSizeSerializerError, + Serializer, +} from '@metaplex-foundation/umi-serializers-core'; +import { + NumberSerializer, + u8, +} from '@metaplex-foundation/umi-serializers-numbers'; + +/** + * Defines the options for boolean serializers. + * @category Serializers + */ +export type BoolSerializerOptions = BaseSerializerOptions & { + /** + * The number serializer to delegate to. + * @defaultValue `u8()` + */ + size?: NumberSerializer; +}; +/** + * Creates a boolean serializer. + * + * @param options - A set of options for the serializer. + * @category Serializers + */ export function bool(options: BoolSerializerOptions = {}): Serializer { const size = options.size ?? u8(); if (size.fixedSize === null) { - throw new DataViewSerializerError( + throw new ExpectedFixedSizeSerializerError( 'Serializer [bool] requires a fixed size.' ); } diff --git a/packages/umi-serializer-data-view/src/bytes.ts b/packages/umi-serializers/src/bytes.ts similarity index 64% rename from packages/umi-serializer-data-view/src/bytes.ts rename to packages/umi-serializers/src/bytes.ts index 403bb134..bdefeb6d 100644 --- a/packages/umi-serializer-data-view/src/bytes.ts +++ b/packages/umi-serializers/src/bytes.ts @@ -1,12 +1,35 @@ import { - BytesSerializerOptions, + BaseSerializerOptions, + DeserializingEmptyBufferError, + NotEnoughBytesError, Serializer, fixSerializer, mergeBytes, -} from '@metaplex-foundation/umi'; -import { DeserializingEmptyBufferError, NotEnoughBytesError } from './errors'; -import { getSizeDescription } from './getSizeDescription'; +} from '@metaplex-foundation/umi-serializers-core'; +import { NumberSerializer } from '@metaplex-foundation/umi-serializers-numbers'; +import { getSizeDescription } from './utils'; +/** + * Defines the options for bytes serializers. + * @category Serializers + */ +export type BytesSerializerOptions = BaseSerializerOptions & { + /** + * The size of the buffer. It can be one of the following: + * - a {@link NumberSerializer} that prefixes the buffer with its size. + * - a fixed number of bytes. + * - or `'variable'` to use the rest of the buffer. + * @defaultValue `'variable'` + */ + size?: NumberSerializer | number | 'variable'; +}; + +/** + * Creates a serializer that passes the buffer as-is. + * + * @param options - A set of options for the serializer. + * @category Serializers + */ export function bytes( options: BytesSerializerOptions = {} ): Serializer { @@ -20,7 +43,7 @@ export function bytes( maxSize: null, serialize: (value: Uint8Array) => new Uint8Array(value), deserialize: (bytes: Uint8Array, offset = 0) => { - const slice = new Uint8Array(bytes.slice(offset)); + const slice = bytes.slice(offset); return [slice, offset + slice.length]; }, }; diff --git a/packages/umi-serializers/src/dataEnum.ts b/packages/umi-serializers/src/dataEnum.ts new file mode 100644 index 00000000..65e54fd9 --- /dev/null +++ b/packages/umi-serializers/src/dataEnum.ts @@ -0,0 +1,170 @@ +import { + BaseSerializerOptions, + DeserializingEmptyBufferError, + Serializer, + mergeBytes, +} from '@metaplex-foundation/umi-serializers-core'; +import { + NumberSerializer, + u8, +} from '@metaplex-foundation/umi-serializers-numbers'; +import { + EnumDiscriminatorOutOfRangeError, + InvalidDataEnumVariantError, +} from './errors'; +import { maxSerializerSizes } from './maxSerializerSizes'; +import { sumSerializerSizes } from './sumSerializerSizes'; + +/** + * Defines a data enum using discriminated union types. + * + * @example + * ```ts + * type WebPageEvent = + * | { __kind: 'pageview', url: string } + * | { __kind: 'click', x: number, y: number }; + * ``` + * + * @category Serializers + */ +export type DataEnum = { __kind: string }; + +/** + * Extracts a variant from a data enum. + * + * @example + * ```ts + * type WebPageEvent = + * | { __kind: 'pageview', url: string } + * | { __kind: 'click', x: number, y: number }; + * type ClickEvent = GetDataEnumKind; + * // -> { __kind: 'click', x: number, y: number } + * ``` + * + * @category Serializers + */ +export type GetDataEnumKind< + T extends DataEnum, + K extends T['__kind'] +> = Extract; + +/** + * Extracts a variant from a data enum without its discriminator. + * + * @example + * ```ts + * type WebPageEvent = + * | { __kind: 'pageview', url: string } + * | { __kind: 'click', x: number, y: number }; + * type ClickEvent = GetDataEnumKindContent; + * // -> { x: number, y: number } + * ``` + * + * @category Serializers + */ +export type GetDataEnumKindContent< + T extends DataEnum, + K extends T['__kind'] +> = Omit, '__kind'>; + +/** + * Get the name and serializer of each variant in a data enum. + * @category Serializers + */ +export type DataEnumToSerializerTuple = Array< + T extends any + ? [ + T['__kind'], + keyof Omit extends never + ? Serializer, Omit> | Serializer + : Serializer, Omit> + ] + : never +>; + +/** + * Defines the options for data enum serializers. + * @category Serializers + */ +export type DataEnumSerializerOptions = BaseSerializerOptions & { + /** + * The serializer to use for the enum discriminator prefixing the variant. + * @defaultValue `u8()` + */ + size?: NumberSerializer; +}; + +/** + * Creates a data enum serializer. + * + * @param variants - The variant serializers of the data enum. + * @param options - A set of options for the serializer. + * @category Serializers + */ +export function dataEnum( + variants: DataEnumToSerializerTuple, + options: DataEnumSerializerOptions = {} +): Serializer { + const prefix = options.size ?? u8(); + const fieldDescriptions = variants + .map( + ([name, serializer]) => + `${String(name)}${serializer ? `: ${serializer.description}` : ''}` + ) + .join(', '); + const allVariantHaveTheSameFixedSize = variants.every( + (one, i, all) => one[1].fixedSize === all[0][1].fixedSize + ); + const fixedVariantSize = allVariantHaveTheSameFixedSize + ? variants[0][1].fixedSize + : null; + const maxVariantSize = maxSerializerSizes( + variants.map(([, field]) => field.maxSize) + ); + return { + description: + options.description ?? + `dataEnum(${fieldDescriptions}; ${prefix.description})`, + fixedSize: + variants.length === 0 + ? prefix.fixedSize + : sumSerializerSizes([prefix.fixedSize, fixedVariantSize]), + maxSize: + variants.length === 0 + ? prefix.maxSize + : sumSerializerSizes([prefix.maxSize, maxVariantSize]), + serialize: (variant: T) => { + const discriminator = variants.findIndex( + ([key]) => variant.__kind === key + ); + if (discriminator < 0) { + throw new InvalidDataEnumVariantError( + variant.__kind, + variants.map(([key]) => key) + ); + } + const variantPrefix = prefix.serialize(discriminator); + const variantSerializer = variants[discriminator][1]; + const variantBytes = variantSerializer.serialize(variant as any); + return mergeBytes([variantPrefix, variantBytes]); + }, + deserialize: (bytes: Uint8Array, offset = 0) => { + if (bytes.slice(offset).length === 0) { + throw new DeserializingEmptyBufferError('dataEnum'); + } + const [discriminator, dOffset] = prefix.deserialize(bytes, offset); + offset = dOffset; + const variantField = variants[Number(discriminator)] ?? null; + if (!variantField) { + throw new EnumDiscriminatorOutOfRangeError( + discriminator, + 0, + variants.length - 1 + ); + } + const [variant, vOffset] = variantField[1].deserialize(bytes, offset); + offset = vOffset; + return [{ __kind: variantField[0], ...(variant ?? {}) } as U, offset]; + }, + }; +} diff --git a/packages/umi-serializers/src/errors.ts b/packages/umi-serializers/src/errors.ts new file mode 100644 index 00000000..a87640b2 --- /dev/null +++ b/packages/umi-serializers/src/errors.ts @@ -0,0 +1,82 @@ +/** @category Errors */ +export class InvalidNumberOfItemsError extends Error { + readonly name = 'InvalidNumberOfItemsError'; + + constructor( + serializer: string, + expected: number | bigint, + actual: number | bigint + ) { + super(`Expected [${serializer}] to have ${expected} items, got ${actual}.`); + } +} + +/** @category Errors */ +export class InvalidArrayLikeRemainderSizeError extends Error { + readonly name = 'InvalidArrayLikeRemainderSizeError'; + + constructor(remainderSize: number | bigint, itemSize: number | bigint) { + super( + `The remainder of the buffer (${remainderSize} bytes) cannot be split into chunks of ${itemSize} bytes. ` + + `Serializers of "remainder" size must have a remainder that is a multiple of its item size. ` + + `In other words, ${remainderSize} modulo ${itemSize} should be equal to zero.` + ); + } +} + +/** @category Errors */ +export class UnrecognizedArrayLikeSerializerSizeError extends Error { + readonly name = 'UnrecognizedArrayLikeSerializerSizeError'; + + constructor(size: never) { + super(`Unrecognized array-like serializer size: ${JSON.stringify(size)}`); + } +} + +/** @category Errors */ +export class InvalidDataEnumVariantError extends Error { + readonly name = 'InvalidDataEnumVariantError'; + + constructor(invalidVariant: string, validVariants: string[]) { + super( + `Invalid data enum variant. ` + + `Expected one of [${validVariants.join(', ')}], ` + + `got "${invalidVariant}".` + ); + } +} + +/** @category Errors */ +export class InvalidScalarEnumVariantError extends Error { + readonly name = 'InvalidScalarEnumVariantError'; + + constructor( + invalidVariant: string | number | bigint, + validVariants: string[], + min: number | bigint, + max: number | bigint + ) { + super( + `Invalid scalar enum variant. ` + + `Expected one of [${validVariants.join(', ')}] ` + + `or a number between ${min} and ${max}, ` + + `got "${invalidVariant}".` + ); + } +} + +/** @category Errors */ +export class EnumDiscriminatorOutOfRangeError extends RangeError { + readonly name = 'EnumDiscriminatorOutOfRangeError'; + + constructor( + discriminator: number | bigint, + min: number | bigint, + max: number | bigint + ) { + super( + `Enum discriminator out of range. ` + + `Expected a number between ${min} and ${max}, got ${discriminator}.` + ); + } +} diff --git a/packages/umi-serializers/src/index.ts b/packages/umi-serializers/src/index.ts new file mode 100644 index 00000000..521ccb3d --- /dev/null +++ b/packages/umi-serializers/src/index.ts @@ -0,0 +1,24 @@ +export * from '@metaplex-foundation/umi-serializers-core'; +export * from '@metaplex-foundation/umi-serializers-encodings'; +export * from '@metaplex-foundation/umi-serializers-numbers'; + +export * from './array'; +export * from './bitArray'; +export * from './bool'; +export * from './bytes'; +export * from './dataEnum'; +export * from './errors'; +export * from './map'; +export * from './nullable'; +export * from './option'; +export * from './publicKey'; +export * from './scalarEnum'; +export * from './set'; +export * from './string'; +export * from './struct'; +export * from './tuple'; +export * from './unit'; + +export * from './arrayLikeSerializerSize'; +export * from './maxSerializerSizes'; +export * from './sumSerializerSizes'; diff --git a/packages/umi-serializer-data-view/src/map.ts b/packages/umi-serializers/src/map.ts similarity index 63% rename from packages/umi-serializer-data-view/src/map.ts rename to packages/umi-serializers/src/map.ts index 42f93b93..044e5d24 100644 --- a/packages/umi-serializer-data-view/src/map.ts +++ b/packages/umi-serializers/src/map.ts @@ -1,18 +1,39 @@ import { - MapSerializerOptions, + BaseSerializerOptions, + ExpectedFixedSizeSerializerError, mergeBytes, Serializer, -} from '@metaplex-foundation/umi'; +} from '@metaplex-foundation/umi-serializers-core'; +import { u32 } from '@metaplex-foundation/umi-serializers-numbers'; +import { ArrayLikeSerializerSize } from './arrayLikeSerializerSize'; import { - DataViewSerializerError, - DeserializingEmptyBufferError, -} from './errors'; -import { getResolvedSize } from './getResolvedSize'; -import { getSizeDescription } from './getSizeDescription'; -import { getSizeFromChildren } from './getSizeFromChildren'; -import { getSizePrefix } from './getSizePrefix'; -import { u32 } from './numbers'; + getResolvedSize, + getSizeDescription, + getSizeFromChildren, + getSizePrefix, +} from './utils'; +import { InvalidNumberOfItemsError } from './errors'; +/** + * Defines the options for `Map` serializers. + * @category Serializers + */ +export type MapSerializerOptions = BaseSerializerOptions & { + /** + * The size of the map. + * @defaultValue `u32()` + */ + size?: ArrayLikeSerializerSize; +}; + +/** + * Creates a serializer for a map. + * + * @param key - The serializer to use for the map's keys. + * @param value - The serializer to use for the map's values. + * @param options - A set of options for the serializer. + * @category Serializers + */ export function map( key: Serializer, value: Serializer, @@ -23,7 +44,7 @@ export function map( size === 'remainder' && (key.fixedSize === null || value.fixedSize === null) ) { - throw new DataViewSerializerError( + throw new ExpectedFixedSizeSerializerError( 'Serializers of "remainder" size must have fixed-size items.' ); } @@ -37,9 +58,7 @@ export function map( maxSize: getSizeFromChildren(size, [key.maxSize, value.maxSize]), serialize: (map: Map) => { if (typeof size === 'number' && map.size !== size) { - throw new DataViewSerializerError( - `Expected map to have ${size} items but got ${map.size}.` - ); + throw new InvalidNumberOfItemsError('map', size, map.size); } const itemBytes = Array.from(map, ([k, v]) => mergeBytes([key.serialize(k), value.serialize(v)]) @@ -49,7 +68,7 @@ export function map( deserialize: (bytes: Uint8Array, offset = 0) => { const map: Map = new Map(); if (typeof size === 'object' && bytes.slice(offset).length === 0) { - throw new DeserializingEmptyBufferError('map', new Map()); + return [map, offset]; } const [resolvedSize, newOffset] = getResolvedSize( size, diff --git a/packages/umi-serializer-data-view/src/maxSerializerSizes.ts b/packages/umi-serializers/src/maxSerializerSizes.ts similarity index 100% rename from packages/umi-serializer-data-view/src/maxSerializerSizes.ts rename to packages/umi-serializers/src/maxSerializerSizes.ts diff --git a/packages/umi-serializer-data-view/src/nullable.ts b/packages/umi-serializers/src/nullable.ts similarity index 62% rename from packages/umi-serializer-data-view/src/nullable.ts rename to packages/umi-serializers/src/nullable.ts index 9ea1ae4a..d08e57e3 100644 --- a/packages/umi-serializer-data-view/src/nullable.ts +++ b/packages/umi-serializers/src/nullable.ts @@ -1,17 +1,45 @@ +import { Nullable } from '@metaplex-foundation/umi-options'; import { + BaseSerializerOptions, + ExpectedFixedSizeSerializerError, Serializer, - NullableSerializerOptions, - Nullable, mergeBytes, -} from '@metaplex-foundation/umi'; +} from '@metaplex-foundation/umi-serializers-core'; import { - DataViewSerializerError, - DeserializingEmptyBufferError, -} from './errors'; -import { getSizeDescription } from './getSizeDescription'; -import { u8 } from './numbers'; + NumberSerializer, + u8, +} from '@metaplex-foundation/umi-serializers-numbers'; import { sumSerializerSizes } from './sumSerializerSizes'; +import { getSizeDescription } from './utils'; +/** + * Defines the options for `Nullable` serializers. + * @category Serializers + */ +export type NullableSerializerOptions = BaseSerializerOptions & { + /** + * The serializer to use for the boolean prefix. + * @defaultValue `u8()` + */ + prefix?: NumberSerializer; + /** + * Whether the item serializer should be of fixed size. + * + * When this is true, a `null` value will skip the bytes that would + * have been used for the item. Note that this will only work if the + * item serializer is of fixed size. + * @defaultValue `false` + */ + fixed?: boolean; +}; + +/** + * Creates a serializer for an optional value using `null` as the `None` value. + * + * @param item - The serializer to use for the value that may be present. + * @param options - A set of options for the serializer. + * @category Serializers + */ export function nullable( item: Serializer, options: NullableSerializerOptions = {} @@ -22,7 +50,7 @@ export function nullable( let fixedSize = item.fixedSize === 0 ? prefix.fixedSize : null; if (fixed) { if (item.fixedSize === null || prefix.fixedSize === null) { - throw new DataViewSerializerError( + throw new ExpectedFixedSizeSerializerError( 'Fixed nullables can only be used with fixed-size serializers' ); } @@ -51,7 +79,7 @@ export function nullable( }, deserialize: (bytes: Uint8Array, offset = 0) => { if (bytes.slice(offset).length === 0) { - throw new DeserializingEmptyBufferError('nullable', null); + return [null, offset]; } const fixedOffset = offset + (prefix.fixedSize ?? 0) + (item.fixedSize ?? 0); diff --git a/packages/umi-serializer-data-view/src/option.ts b/packages/umi-serializers/src/option.ts similarity index 61% rename from packages/umi-serializer-data-view/src/option.ts rename to packages/umi-serializers/src/option.ts index 51ae812c..94b88728 100644 --- a/packages/umi-serializer-data-view/src/option.ts +++ b/packages/umi-serializers/src/option.ts @@ -1,34 +1,64 @@ import { - Serializer, Option, - OptionSerializerOptions, + OptionOrNullable, + isOption, isSome, - mergeBytes, none, some, - Nullable, - isOption, wrapNullable, -} from '@metaplex-foundation/umi'; +} from '@metaplex-foundation/umi-options'; import { - DataViewSerializerError, - DeserializingEmptyBufferError, -} from './errors'; -import { getSizeDescription } from './getSizeDescription'; -import { u8 } from './numbers'; + BaseSerializerOptions, + ExpectedFixedSizeSerializerError, + Serializer, + mergeBytes, +} from '@metaplex-foundation/umi-serializers-core'; +import { + NumberSerializer, + u8, +} from '@metaplex-foundation/umi-serializers-numbers'; import { sumSerializerSizes } from './sumSerializerSizes'; +import { getSizeDescription } from './utils'; + +/** + * Defines the options for `Option` serializers. + * @category Serializers + */ +export type OptionSerializerOptions = BaseSerializerOptions & { + /** + * The serializer to use for the boolean prefix. + * @defaultValue `u8()` + */ + prefix?: NumberSerializer; + /** + * Whether the item serializer should be of fixed size. + * + * When this is true, a `None` value will skip the bytes that would + * have been used for the item. Note that this will only work if the + * item serializer is of fixed size. + * @defaultValue `false` + */ + fixed?: boolean; +}; +/** + * Creates a serializer for an optional value using the {@link Option} type. + * + * @param item - The serializer to use for the value that may be present. + * @param options - A set of options for the serializer. + * @category Serializers + */ export function option( item: Serializer, options: OptionSerializerOptions = {} -): Serializer | Nullable, Option> { +): Serializer, Option> { const prefix = options.prefix ?? u8(); const fixed = options.fixed ?? false; let descriptionSuffix = `; ${getSizeDescription(prefix)}`; let fixedSize = item.fixedSize === 0 ? prefix.fixedSize : null; if (fixed) { if (item.fixedSize === null || prefix.fixedSize === null) { - throw new DataViewSerializerError( + throw new ExpectedFixedSizeSerializerError( 'Fixed options can only be used with fixed-size serializers' ); } @@ -40,7 +70,7 @@ export function option( options.description ?? `option(${item.description + descriptionSuffix})`, fixedSize, maxSize: sumSerializerSizes([prefix.maxSize, item.maxSize]), - serialize: (optionOrNullable: Option | Nullable) => { + serialize: (optionOrNullable: OptionOrNullable) => { const option = isOption(optionOrNullable) ? optionOrNullable : wrapNullable(optionOrNullable); @@ -60,7 +90,7 @@ export function option( }, deserialize: (bytes: Uint8Array, offset = 0) => { if (bytes.slice(offset).length === 0) { - throw new DeserializingEmptyBufferError('option', none()); + return [none(), offset]; } const fixedOffset = offset + (prefix.fixedSize ?? 0) + (item.fixedSize ?? 0); diff --git a/packages/umi-serializer-data-view/src/pubkey.ts b/packages/umi-serializers/src/publicKey.ts similarity index 64% rename from packages/umi-serializer-data-view/src/pubkey.ts rename to packages/umi-serializers/src/publicKey.ts index 4794bb9e..8176ae67 100644 --- a/packages/umi-serializer-data-view/src/pubkey.ts +++ b/packages/umi-serializers/src/publicKey.ts @@ -2,13 +2,28 @@ import { PUBLIC_KEY_LENGTH, PublicKey, PublicKeyInput, - PublicKeySerializerOptions, - Serializer, publicKeyBytes, publicKey as toPublicKey, -} from '@metaplex-foundation/umi'; -import { DeserializingEmptyBufferError, NotEnoughBytesError } from './errors'; +} from '@metaplex-foundation/umi-public-keys'; +import { + BaseSerializerOptions, + DeserializingEmptyBufferError, + NotEnoughBytesError, + Serializer, +} from '@metaplex-foundation/umi-serializers-core'; + +/** + * Defines the options for `PublicKey` serializers. + * @category Serializers + */ +export type PublicKeySerializerOptions = BaseSerializerOptions; +/** + * Creates a serializer for base58 encoded public keys. + * + * @param options - A set of options for the serializer. + * @category Serializers + */ export function publicKey( options: PublicKeySerializerOptions = {} ): Serializer { diff --git a/packages/umi-serializer-data-view/src/scalarEnum.ts b/packages/umi-serializers/src/scalarEnum.ts similarity index 59% rename from packages/umi-serializer-data-view/src/scalarEnum.ts rename to packages/umi-serializers/src/scalarEnum.ts index 1a5f4690..0803f169 100644 --- a/packages/umi-serializer-data-view/src/scalarEnum.ts +++ b/packages/umi-serializers/src/scalarEnum.ts @@ -1,17 +1,55 @@ import { - ScalarEnum, - EnumSerializerOptions, + BaseSerializerOptions, + DeserializingEmptyBufferError, Serializer, -} from '@metaplex-foundation/umi'; +} from '@metaplex-foundation/umi-serializers-core'; import { - DataViewSerializerError, - DeserializingEmptyBufferError, + NumberSerializer, + u8, +} from '@metaplex-foundation/umi-serializers-numbers'; +import { + EnumDiscriminatorOutOfRangeError, + InvalidScalarEnumVariantError, } from './errors'; -import { u8 } from './numbers'; +/** + * Defines a scalar enum as a type from its constructor. + * + * @example + * ```ts + * enum Direction { Left, Right }; + * type DirectionType = ScalarEnum; + * ``` + * + * @category Serializers + */ +export type ScalarEnum = + | { [key: number | string]: string | number | T } + | number + | T; + +/** + * Defines the options for scalar enum serializers. + * @category Serializers + */ +export type ScalarEnumSerializerOptions = BaseSerializerOptions & { + /** + * The serializer to use for the enum discriminator. + * @defaultValue `u8()` + */ + size?: NumberSerializer; +}; + +/** + * Creates a scalar enum serializer. + * + * @param constructor - The constructor of the scalar enum. + * @param options - A set of options for the serializer. + * @category Serializers + */ export function scalarEnum( constructor: ScalarEnum & {}, - options: EnumSerializerOptions = {} + options: ScalarEnumSerializerOptions = {} ): Serializer { const prefix = options.size ?? u8(); const enumKeys = Object.keys(constructor); @@ -33,10 +71,11 @@ export function scalarEnum( const isInvalidString = typeof variant === 'string' && !stringValues.includes(variant); if (isInvalidNumber || isInvalidString) { - throw new DataViewSerializerError( - `Invalid enum variant. Got "${variant}", ` + - `expected one of [${stringValues.join(', ')}] ` + - `or a number between ${minRange} and ${maxRange}` + throw new InvalidScalarEnumVariantError( + variant, + stringValues, + minRange, + maxRange ); } } @@ -60,7 +99,13 @@ export function scalarEnum( const [value, newOffset] = prefix.deserialize(bytes, offset); const valueAsNumber = Number(value); offset = newOffset; - assertValidVariant(valueAsNumber); + if (valueAsNumber < minRange || valueAsNumber > maxRange) { + throw new EnumDiscriminatorOutOfRangeError( + valueAsNumber, + minRange, + maxRange + ); + } return [ (isNumericEnum ? valueAsNumber : enumValues[valueAsNumber]) as T, offset, diff --git a/packages/umi-serializer-data-view/src/set.ts b/packages/umi-serializers/src/set.ts similarity index 58% rename from packages/umi-serializer-data-view/src/set.ts rename to packages/umi-serializers/src/set.ts index 0b4b2e85..01ff42c2 100644 --- a/packages/umi-serializer-data-view/src/set.ts +++ b/packages/umi-serializers/src/set.ts @@ -1,25 +1,45 @@ import { + BaseSerializerOptions, + ExpectedFixedSizeSerializerError, mergeBytes, Serializer, - SetSerializerOptions, -} from '@metaplex-foundation/umi'; +} from '@metaplex-foundation/umi-serializers-core'; +import { u32 } from '@metaplex-foundation/umi-serializers-numbers'; +import { ArrayLikeSerializerSize } from './arrayLikeSerializerSize'; import { - DataViewSerializerError, - DeserializingEmptyBufferError, -} from './errors'; -import { getResolvedSize } from './getResolvedSize'; -import { getSizeDescription } from './getSizeDescription'; -import { getSizeFromChildren } from './getSizeFromChildren'; -import { getSizePrefix } from './getSizePrefix'; -import { u32 } from './numbers'; + getResolvedSize, + getSizeDescription, + getSizeFromChildren, + getSizePrefix, +} from './utils'; +import { InvalidNumberOfItemsError } from './errors'; +/** + * Defines the options for `Set` serializers. + * @category Serializers + */ +export type SetSerializerOptions = BaseSerializerOptions & { + /** + * The size of the set. + * @defaultValue `u32()` + */ + size?: ArrayLikeSerializerSize; +}; + +/** + * Creates a serializer for a set. + * + * @param item - The serializer to use for the set's items. + * @param options - A set of options for the serializer. + * @category Serializers + */ export function set( item: Serializer, options: SetSerializerOptions = {} ): Serializer, Set> { const size = options.size ?? u32(); if (size === 'remainder' && item.fixedSize === null) { - throw new DataViewSerializerError( + throw new ExpectedFixedSizeSerializerError( 'Serializers of "remainder" size must have fixed-size items.' ); } @@ -31,9 +51,7 @@ export function set( maxSize: getSizeFromChildren(size, [item.maxSize]), serialize: (set: Set) => { if (typeof size === 'number' && set.size !== size) { - throw new DataViewSerializerError( - `Expected set to have ${size} items but got ${set.size}.` - ); + throw new InvalidNumberOfItemsError('set', size, set.size); } const itemBytes = Array.from(set, (value) => item.serialize(value)); return mergeBytes([getSizePrefix(size, set.size), ...itemBytes]); @@ -41,7 +59,7 @@ export function set( deserialize: (bytes: Uint8Array, offset = 0) => { const set: Set = new Set(); if (typeof size === 'object' && bytes.slice(offset).length === 0) { - throw new DeserializingEmptyBufferError('set', new Set()); + return [set, offset]; } const [resolvedSize, newOffset] = getResolvedSize( size, diff --git a/packages/umi-serializer-data-view/src/string.ts b/packages/umi-serializers/src/string.ts similarity index 57% rename from packages/umi-serializer-data-view/src/string.ts rename to packages/umi-serializers/src/string.ts index 5866b469..af1a96b8 100644 --- a/packages/umi-serializer-data-view/src/string.ts +++ b/packages/umi-serializers/src/string.ts @@ -1,14 +1,44 @@ import { - StringSerializerOptions, + BaseSerializerOptions, + DeserializingEmptyBufferError, + NotEnoughBytesError, Serializer, - utf8, fixSerializer, mergeBytes, -} from '@metaplex-foundation/umi'; -import { DeserializingEmptyBufferError, NotEnoughBytesError } from './errors'; -import { getSizeDescription } from './getSizeDescription'; -import { u32 } from './numbers'; +} from '@metaplex-foundation/umi-serializers-core'; +import { utf8 } from '@metaplex-foundation/umi-serializers-encodings'; +import { + NumberSerializer, + u32, +} from '@metaplex-foundation/umi-serializers-numbers'; +import { getSizeDescription } from './utils'; + +/** + * Defines the options for string serializers. + * @category Serializers + */ +export type StringSerializerOptions = BaseSerializerOptions & { + /** + * The size of the string. It can be one of the following: + * - a {@link NumberSerializer} that prefixes the string with its size. + * - a fixed number of bytes. + * - or `'variable'` to use the rest of the buffer. + * @defaultValue `u32()` + */ + size?: NumberSerializer | number | 'variable'; + /** + * The string serializer to use for encoding and decoding the content. + * @defaultValue `utf8` + */ + encoding?: Serializer; +}; +/** + * Creates a string serializer. + * + * @param options - A set of options for the serializer. + * @category Serializers + */ export function string( options: StringSerializerOptions = {} ): Serializer { diff --git a/packages/umi-serializer-data-view/src/struct.ts b/packages/umi-serializers/src/struct.ts similarity index 63% rename from packages/umi-serializer-data-view/src/struct.ts rename to packages/umi-serializers/src/struct.ts index e01c785b..570bb966 100644 --- a/packages/umi-serializer-data-view/src/struct.ts +++ b/packages/umi-serializers/src/struct.ts @@ -1,11 +1,33 @@ import { - StructToSerializerTuple, - StructSerializerOptions, + BaseSerializerOptions, Serializer, mergeBytes, -} from '@metaplex-foundation/umi'; +} from '@metaplex-foundation/umi-serializers-core'; import { sumSerializerSizes } from './sumSerializerSizes'; +/** + * Get the name and serializer of each field in a struct. + * @category Serializers + */ +export type StructToSerializerTuple = Array< + { + [K in keyof T]: [K, Serializer]; + }[keyof T] +>; + +/** + * Defines the options for struct serializers. + * @category Serializers + */ +export type StructSerializerOptions = BaseSerializerOptions; + +/** + * Creates a serializer for a custom object. + * + * @param fields - The name and serializer of each field. + * @param options - A set of options for the serializer. + * @category Serializers + */ export function struct( fields: StructToSerializerTuple, options: StructSerializerOptions = {} diff --git a/packages/umi-serializer-data-view/src/sumSerializerSizes.ts b/packages/umi-serializers/src/sumSerializerSizes.ts similarity index 100% rename from packages/umi-serializer-data-view/src/sumSerializerSizes.ts rename to packages/umi-serializers/src/sumSerializerSizes.ts diff --git a/packages/umi-serializer-data-view/src/tuple.ts b/packages/umi-serializers/src/tuple.ts similarity index 64% rename from packages/umi-serializer-data-view/src/tuple.ts rename to packages/umi-serializers/src/tuple.ts index 19db46dd..2663b46d 100644 --- a/packages/umi-serializer-data-view/src/tuple.ts +++ b/packages/umi-serializers/src/tuple.ts @@ -1,12 +1,25 @@ import { WrapInSerializer, - TupleSerializerOptions, Serializer, mergeBytes, -} from '@metaplex-foundation/umi'; -import { DataViewSerializerError } from './errors'; + BaseSerializerOptions, +} from '@metaplex-foundation/umi-serializers-core'; import { sumSerializerSizes } from './sumSerializerSizes'; +import { InvalidNumberOfItemsError } from './errors'; +/** + * Defines the options for tuple serializers. + * @category Serializers + */ +export type TupleSerializerOptions = BaseSerializerOptions; + +/** + * Creates a serializer for a tuple-like array. + * + * @param items - The serializers to use for each item in the tuple. + * @param options - A set of options for the serializer. + * @category Serializers + */ export function tuple( items: WrapInSerializer<[...T], [...U]>, options: TupleSerializerOptions = {} @@ -18,8 +31,10 @@ export function tuple( maxSize: sumSerializerSizes(items.map((item) => item.maxSize)), serialize: (value: T) => { if (value.length !== items.length) { - throw new DataViewSerializerError( - `Expected tuple to have ${items.length} items but got ${value.length}.` + throw new InvalidNumberOfItemsError( + 'tuple', + items.length, + value.length ); } return mergeBytes( diff --git a/packages/umi-serializers/src/unit.ts b/packages/umi-serializers/src/unit.ts new file mode 100644 index 00000000..bc739917 --- /dev/null +++ b/packages/umi-serializers/src/unit.ts @@ -0,0 +1,25 @@ +import { + BaseSerializerOptions, + Serializer, +} from '@metaplex-foundation/umi-serializers-core'; + +/** + * Defines the options for unit serializers. + * @category Serializers + */ +export type UnitSerializerOptions = BaseSerializerOptions; + +/** + * Creates a void serializer. + * + * @param options - A set of options for the serializer. + */ +export function unit(options: UnitSerializerOptions = {}): Serializer { + return { + description: options.description ?? 'unit', + fixedSize: 0, + maxSize: 0, + serialize: () => new Uint8Array(), + deserialize: (_bytes: Uint8Array, offset = 0) => [undefined, offset], + }; +} diff --git a/packages/umi-serializers/src/utils.ts b/packages/umi-serializers/src/utils.ts new file mode 100644 index 00000000..edf71a38 --- /dev/null +++ b/packages/umi-serializers/src/utils.ts @@ -0,0 +1,61 @@ +import { ExpectedFixedSizeSerializerError } from '@metaplex-foundation/umi-serializers-core'; +import { ArrayLikeSerializerSize } from './arrayLikeSerializerSize'; +import { + InvalidArrayLikeRemainderSizeError, + UnrecognizedArrayLikeSerializerSizeError, +} from './errors'; +import { sumSerializerSizes } from './sumSerializerSizes'; + +export function getResolvedSize( + size: ArrayLikeSerializerSize, + childrenSizes: (number | null)[], + bytes: Uint8Array, + offset: number +): [number | bigint, number] { + if (typeof size === 'number') { + return [size, offset]; + } + + if (typeof size === 'object') { + return size.deserialize(bytes, offset); + } + + if (size === 'remainder') { + const childrenSize = sumSerializerSizes(childrenSizes); + if (childrenSize === null) { + throw new ExpectedFixedSizeSerializerError( + 'Serializers of "remainder" size must have fixed-size items.' + ); + } + const remainder = bytes.slice(offset).length; + if (remainder % childrenSize !== 0) { + throw new InvalidArrayLikeRemainderSizeError(remainder, childrenSize); + } + return [remainder / childrenSize, offset]; + } + + throw new UnrecognizedArrayLikeSerializerSizeError(size); +} + +export function getSizeDescription( + size: ArrayLikeSerializerSize | string +): string { + return typeof size === 'object' ? size.description : `${size}`; +} + +export function getSizeFromChildren( + size: ArrayLikeSerializerSize, + childrenSizes: (number | null)[] +): number | null { + if (typeof size !== 'number') return null; + if (size === 0) return 0; + const childrenSize = sumSerializerSizes(childrenSizes); + return childrenSize === null ? null : childrenSize * size; +} + +export function getSizePrefix( + size: ArrayLikeSerializerSize, + realSize: number +): Uint8Array { + return typeof size === 'object' ? size.serialize(realSize) : new Uint8Array(); +} diff --git a/packages/umi-serializer-data-view/test/_helpers.ts b/packages/umi-serializers/test/_helpers.ts similarity index 93% rename from packages/umi-serializer-data-view/test/_helpers.ts rename to packages/umi-serializers/test/_helpers.ts index 44233110..b396754b 100644 --- a/packages/umi-serializer-data-view/test/_helpers.ts +++ b/packages/umi-serializers/test/_helpers.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { base16, Serializer } from '@metaplex-foundation/umi'; import { Assertions } from 'ava'; +import { Serializer, base16 } from '../src'; /** Assert serialization using a hex string. */ export const s = ( diff --git a/packages/umi-serializer-data-view/test/array.test.ts b/packages/umi-serializers/test/array.test.ts similarity index 90% rename from packages/umi-serializer-data-view/test/array.test.ts rename to packages/umi-serializers/test/array.test.ts index 203520f5..73deadd8 100644 --- a/packages/umi-serializer-data-view/test/array.test.ts +++ b/packages/umi-serializers/test/array.test.ts @@ -1,9 +1,6 @@ import test from 'ava'; -import { Endian } from '@metaplex-foundation/umi'; -import { array } from '../src/array'; -import { s, d } from './_helpers'; -import { u16, u32, u64, u8 } from '../src/numbers'; -import { string } from '../src/string'; +import { Endian, array, string, u16, u32, u64, u8 } from '../src'; +import { d, s } from './_helpers'; test('prefixed (de)serialization', (t) => { // Empty. @@ -50,12 +47,10 @@ test('fixed (de)serialization', (t) => { // It fails if the array has a different size. t.throws(() => array(u8(), { size: 1 }).serialize([]), { - message: (m: string) => - m.includes('Expected array to have 1 items but got 0.'), + message: 'Expected [array] to have 1 items, got 0.', }); t.throws(() => array(string(), { size: 2 }).serialize(['a', 'b', 'c']), { - message: (m: string) => - m.includes('Expected array to have 2 items but got 3.'), + message: 'Expected [array] to have 2 items, got 3.', }); }); diff --git a/packages/umi-serializers/test/bitArray.test.ts b/packages/umi-serializers/test/bitArray.test.ts new file mode 100644 index 00000000..41d77914 --- /dev/null +++ b/packages/umi-serializers/test/bitArray.test.ts @@ -0,0 +1,63 @@ +import test from 'ava'; +import { base16, bitArray } from '../src'; + +test('it can serialize bit arrays', (t) => { + // Helper method to create array of booleans and bytes + const a = (bits: string) => [...bits].map((bit) => bit === '1'); + const b = (hex: string) => base16.serialize(hex); + + // Single byte, all zeros. + t.deepEqual(bitArray(1).serialize(a('00000000')), b('00')); + t.deepEqual(bitArray(1).deserialize(b('00')), [a('00000000'), 1]); + t.deepEqual(bitArray(1).deserialize(b('ff00'), 1), [a('00000000'), 2]); + + // Single byte, all ones. + t.deepEqual(bitArray(1).serialize(a('11111111')), b('ff')); + t.deepEqual(bitArray(1).deserialize(b('ff')), [a('11111111'), 1]); + t.deepEqual(bitArray(1).deserialize(b('00ff'), 1), [a('11111111'), 2]); + + // Single byte, first 2 bits, forwards. + t.deepEqual(bitArray(1).serialize(a('11000000')), b('c0')); + t.deepEqual(bitArray(1).deserialize(b('c0')), [a('11000000'), 1]); + t.deepEqual(bitArray(1).deserialize(b('ffc0'), 1), [a('11000000'), 2]); + + // Single byte, first 2 bits, backwards. + t.deepEqual(bitArray(1, true).serialize(a('11000000')), b('03')); + t.deepEqual(bitArray(1, true).deserialize(b('03')), [a('11000000'), 1]); + t.deepEqual(bitArray(1, true).deserialize(b('ff03'), 1), [a('11000000'), 2]); + + // Multiple bytes, first 2 bits, forwards. + const bitsA = '110000000000000000000000'; + t.deepEqual(bitArray(3).serialize(a(bitsA)), b('c00000')); + t.deepEqual(bitArray(3).deserialize(b('c00000')), [a(bitsA), 3]); + t.deepEqual(bitArray(3).deserialize(b('ffc00000'), 1), [a(bitsA), 4]); + + // Multiple bytes, first 2 bits, backwards. + t.deepEqual(bitArray(3, true).serialize(a(bitsA)), b('000003')); + t.deepEqual(bitArray(3, true).deserialize(b('000003')), [a(bitsA), 3]); + t.deepEqual(bitArray(3, true).deserialize(b('ff000003'), 1), [a(bitsA), 4]); + + // Multiple bytes, first half bits, forwards. + const bitsB = '111111111111000000000000'; + t.deepEqual(bitArray(3).serialize(a(bitsB)), b('fff000')); + t.deepEqual(bitArray(3).deserialize(b('fff000')), [a(bitsB), 3]); + t.deepEqual(bitArray(3).deserialize(b('00fff000'), 1), [a(bitsB), 4]); + + // Multiple bytes, first half bits, backwards. + t.deepEqual(bitArray(3, true).serialize(a(bitsB)), b('000fff')); + t.deepEqual(bitArray(3, true).deserialize(b('000fff')), [a(bitsB), 3]); + t.deepEqual(bitArray(3, true).deserialize(b('ff000fff'), 1), [a(bitsB), 4]); + + // It pads missing boolean values with false. + t.deepEqual(bitArray(1).serialize(a('101')), b('a0')); + t.deepEqual(bitArray(1).deserialize(b('a0')), [a('10100000'), 1]); + + // It truncates array of booleans if it is too long. + t.deepEqual(bitArray(1).serialize(a('000000001')), b('00')); + t.deepEqual(bitArray(1).deserialize(b('00')), [a('00000000'), 1]); + + // It fails if the buffer is too short. + t.throws(() => bitArray(3).deserialize(b('ff')), { + message: (m) => m.includes('Serializer [bitArray] expected 3 bytes, got 1'), + }); +}); diff --git a/packages/umi-serializer-data-view/test/bool.test.ts b/packages/umi-serializers/test/bool.test.ts similarity index 85% rename from packages/umi-serializer-data-view/test/bool.test.ts rename to packages/umi-serializers/test/bool.test.ts index 1139e058..02ed82ae 100644 --- a/packages/umi-serializer-data-view/test/bool.test.ts +++ b/packages/umi-serializers/test/bool.test.ts @@ -1,8 +1,6 @@ import test from 'ava'; -import { Endian } from '@metaplex-foundation/umi'; -import { bool } from '../src/bool'; -import { s, d } from './_helpers'; -import { u32 } from '../src/numbers'; +import { bool, Endian, u32 } from '../src'; +import { d, s } from './_helpers'; test('serialization', (t) => { s(t, bool(), true, '01'); diff --git a/packages/umi-serializer-data-view/test/bytes.test.ts b/packages/umi-serializers/test/bytes.test.ts similarity index 89% rename from packages/umi-serializer-data-view/test/bytes.test.ts rename to packages/umi-serializers/test/bytes.test.ts index 07551b80..70916bb9 100644 --- a/packages/umi-serializer-data-view/test/bytes.test.ts +++ b/packages/umi-serializers/test/bytes.test.ts @@ -1,8 +1,6 @@ -import { base16, Endian } from '@metaplex-foundation/umi'; import test from 'ava'; +import { Endian, base16, bytes, u16, u8 } from '../src'; import { d, s } from './_helpers'; -import { bytes } from '../src/bytes'; -import { u16, u8 } from '../src/numbers'; test('prefixed (de)serialization', (t) => { const bytesU8 = bytes({ size: u8() }); @@ -13,7 +11,7 @@ test('prefixed (de)serialization', (t) => { // Not enough bytes. t.throws(() => bytesU8.deserialize(base16.serialize('022a')), { - message: (m) => m.includes('Serializer [bytes] expected 2 bytes, got 1.'), + message: 'Serializer [bytes] expected 2 bytes, got 1.', }); }); @@ -31,7 +29,7 @@ test('fixed (de)serialization', (t) => { d(t, bytes5, '0102000000', new Uint8Array([1, 2, 0, 0, 0]), 5); d(t, bytes5, ['ff0102000000', 1], new Uint8Array([1, 2, 0, 0, 0]), 6); t.throws(() => bytes5.deserialize(base16.serialize('0102')), { - message: (m) => m.includes('Fixed serializer expected 5 bytes, got 2'), + message: 'Serializer [fixSerializer] expected 5 bytes, got 2.', }); // Too large (truncated). diff --git a/packages/umi-serializer-data-view/test/dataEnum.test.ts b/packages/umi-serializers/test/dataEnum.test.ts similarity index 89% rename from packages/umi-serializer-data-view/test/dataEnum.test.ts rename to packages/umi-serializers/test/dataEnum.test.ts index 6d940a86..9b9778bf 100644 --- a/packages/umi-serializer-data-view/test/dataEnum.test.ts +++ b/packages/umi-serializers/test/dataEnum.test.ts @@ -1,14 +1,19 @@ import test from 'ava'; -import { DataEnumToSerializerTuple } from '@metaplex-foundation/umi'; -import { array } from '../src/array'; -import { bool } from '../src/bool'; -import { dataEnum } from '../src/dataEnum'; -import { s, d } from './_helpers'; -import { u16, u32, u64, u8 } from '../src/numbers'; -import { string } from '../src/string'; -import { struct } from '../src/struct'; -import { tuple } from '../src/tuple'; -import { unit } from '../src/unit'; +import { + DataEnumToSerializerTuple, + array, + bool, + dataEnum, + string, + struct, + tuple, + u16, + u32, + u64, + u8, + unit, +} from '../src'; +import { d, s } from './_helpers'; type WebEvent = | { __kind: 'PageLoad' } // Empty variant. @@ -98,15 +103,16 @@ test('invalid variant (de)serialization', (t) => { { message: (m: string) => m.includes( - 'Invalid data enum variant. Got "Missing", ' + - 'expected one of [PageLoad, Click, KeyPress, PageUnload]' + 'Invalid data enum variant. ' + + 'Expected one of [PageLoad, Click, KeyPress, PageUnload], ' + + 'got "Missing".' ), } ); t.throws(() => dataEnum(getWebEvent()).deserialize(new Uint8Array([4])), { message: (m: string) => m.includes( - 'Data enum index "4" is out of range. Index should be between 0 and 3.' + 'Enum discriminator out of range. Expected a number between 0 and 3, got 4.' ), }); }); diff --git a/packages/umi-serializers/test/empty.test.ts b/packages/umi-serializers/test/empty.test.ts new file mode 100644 index 00000000..f3aa1bc7 --- /dev/null +++ b/packages/umi-serializers/test/empty.test.ts @@ -0,0 +1,96 @@ +import { none } from '@metaplex-foundation/umi-options'; +import test, { ThrowsExpectation } from 'ava'; +import { + Serializer, + array, + bool, + bytes, + dataEnum, + map, + nullable, + option, + publicKey, + scalarEnum, + set, + string, + struct, + tuple, + u64, + u8, + unit, +} from '../src'; + +test('it can handle empty buffers', (t) => { + const e: ThrowsExpectation = { name: 'DeserializingEmptyBufferError' }; + const fixedError = (expectedBytes: number): ThrowsExpectation => ({ + message: `Serializer [fixSerializer] expected ${expectedBytes} bytes, got 0.`, + }); + const empty = (serializer: Serializer) => + serializer.deserialize(new Uint8Array())[0]; + + // Tuple. + t.throws(() => empty(tuple([u8()])), e); + t.deepEqual(empty(tuple([])), []); + + // Array. + t.deepEqual(empty(array(u8())), []); + t.deepEqual(empty(array(u8(), { size: 'remainder' })), []); + t.throws(() => empty(array(u8(), { size: 5 })), e); + t.deepEqual(empty(array(u8(), { size: 0 })), []); + + // Map. + t.deepEqual(empty(map(u8(), u8())), new Map()); + t.deepEqual(empty(map(u8(), u8(), { size: 'remainder' })), new Map()); + t.throws(() => empty(map(u8(), u8(), { size: 5 })), e); + t.deepEqual(empty(map(u8(), u8(), { size: 0 })), new Map()); + + // Set. + t.deepEqual(empty(set(u8())), new Set()); + t.deepEqual(empty(set(u8(), { size: 'remainder' })), new Set()); + t.throws(() => empty(set(u8(), { size: 5 })), e); + t.deepEqual(empty(set(u8(), { size: 0 })), new Set()); + + // Option. + t.deepEqual(empty(option(u8())), none()); + t.deepEqual(empty(option(u8(), { fixed: true })), none()); + + // Nullable. + t.deepEqual(empty(nullable(u8())), null); + t.deepEqual(empty(nullable(u8(), { fixed: true })), null); + + // Struct. + t.throws(() => empty(struct([['age', u8()]])), e); + t.deepEqual(empty(struct([])), {}); + + // ScalarEnum. + enum DummyEnum {} + t.throws(() => empty(scalarEnum(DummyEnum)), e); + + // DataEnum. + type DummyDataEnum = { __kind: 'foo' }; + t.throws(() => empty(dataEnum([['foo', unit()]])), e); + + // Strings. + t.throws(() => empty(string()), e); + t.throws(() => empty(string({ size: 5 })), fixedError(5)); + t.is(empty(string({ size: 0 })), ''); + + // Bool. + t.throws(() => empty(bool()), e); + + // Unit. + t.is(empty(unit()), undefined); + + // Numbers. + t.throws(() => empty(u8()), e); + t.throws(() => empty(u64()), e); + + // PublicKey. + t.throws(() => empty(publicKey()), e); + + // Bytes. + t.deepEqual(empty(bytes()), new Uint8Array()); + t.deepEqual(empty(bytes({ size: 'variable' })), new Uint8Array()); + t.throws(() => empty(bytes({ size: u8() })), e); + t.throws(() => empty(bytes({ size: 5 })), fixedError(5)); +}); diff --git a/packages/umi-serializer-data-view/test/map.test.ts b/packages/umi-serializers/test/map.test.ts similarity index 93% rename from packages/umi-serializer-data-view/test/map.test.ts rename to packages/umi-serializers/test/map.test.ts index 8c6d4d9f..fcc0e3ee 100644 --- a/packages/umi-serializer-data-view/test/map.test.ts +++ b/packages/umi-serializers/test/map.test.ts @@ -1,9 +1,6 @@ import test from 'ava'; -import { Endian } from '@metaplex-foundation/umi'; -import { map } from '../src/map'; -import { s, d } from './_helpers'; -import { u16, u32, u64, u8 } from '../src/numbers'; -import { string } from '../src/string'; +import { Endian, map, string, u16, u32, u64, u8 } from '../src'; +import { d, s } from './_helpers'; test('prefixed (de)serialization', (t) => { // Empty. @@ -62,11 +59,11 @@ test('fixed (de)serialization', (t) => { // It fails if the map has a different size. t.throws(() => map(u8(), u8(), { size: 1 }).serialize(new Map()), { message: (m: string) => - m.includes('Expected map to have 1 items but got 0.'), + m.includes('Expected [map] to have 1 items, got 0.'), }); t.throws(() => letters.serialize(lettersMap.set('c', 3)), { message: (m: string) => - m.includes('Expected map to have 2 items but got 3.'), + m.includes('Expected [map] to have 2 items, got 3.'), }); }); diff --git a/packages/umi-serializer-data-view/test/nullable.test.ts b/packages/umi-serializers/test/nullable.test.ts similarity index 96% rename from packages/umi-serializer-data-view/test/nullable.test.ts rename to packages/umi-serializers/test/nullable.test.ts index 24a85409..4319d8e0 100644 --- a/packages/umi-serializer-data-view/test/nullable.test.ts +++ b/packages/umi-serializers/test/nullable.test.ts @@ -1,8 +1,6 @@ import test from 'ava'; -import { nullable } from '../src/nullable'; +import { nullable, string, u16, u64, u8 } from '../src'; import { d, s } from './_helpers'; -import { u16, u64, u8 } from '../src/numbers'; -import { string } from '../src/string'; test('regular (de)serialization', (t) => { // Null. diff --git a/packages/umi-serializer-data-view/test/option.test.ts b/packages/umi-serializers/test/option.test.ts similarity index 96% rename from packages/umi-serializer-data-view/test/option.test.ts rename to packages/umi-serializers/test/option.test.ts index 2f0c2b8c..8d883dae 100644 --- a/packages/umi-serializer-data-view/test/option.test.ts +++ b/packages/umi-serializers/test/option.test.ts @@ -1,9 +1,7 @@ +import { none, some } from '@metaplex-foundation/umi-options'; import test from 'ava'; -import { none, some } from '@metaplex-foundation/umi'; -import { option } from '../src/option'; -import { s, d } from './_helpers'; -import { u16, u64, u8 } from '../src/numbers'; -import { string } from '../src/string'; +import { option, string, u16, u64, u8 } from '../src'; +import { d, s } from './_helpers'; test('regular (de)serialization', (t) => { // None. diff --git a/packages/umi-serializer-data-view/test/publicKey.test.ts b/packages/umi-serializers/test/publicKey.test.ts similarity index 91% rename from packages/umi-serializer-data-view/test/publicKey.test.ts rename to packages/umi-serializers/test/publicKey.test.ts index d9a5bc6b..0e815ca2 100644 --- a/packages/umi-serializer-data-view/test/publicKey.test.ts +++ b/packages/umi-serializers/test/publicKey.test.ts @@ -1,12 +1,10 @@ -import test from 'ava'; import { - base16, publicKeyBytes, publicKey as toPublicKey, -} from '@metaplex-foundation/umi'; - -import { s, d } from './_helpers'; -import { publicKey } from '../src/pubkey'; +} from '@metaplex-foundation/umi-public-keys'; +import test from 'ava'; +import { base16, publicKey } from '../src'; +import { d, s } from './_helpers'; test('serialization', (t) => { const keyA = toPublicKey('4HM9LW2rm3SR2ZdBiFK3D21ENmQWpqEJEhx1nfgcC3r9'); diff --git a/packages/umi-serializer-data-view/test/enum.test.ts b/packages/umi-serializers/test/scalarEnum.test.ts similarity index 84% rename from packages/umi-serializer-data-view/test/enum.test.ts rename to packages/umi-serializers/test/scalarEnum.test.ts index c084ad10..6ea20d39 100644 --- a/packages/umi-serializer-data-view/test/enum.test.ts +++ b/packages/umi-serializers/test/scalarEnum.test.ts @@ -1,7 +1,6 @@ import test from 'ava'; -import { s, d } from './_helpers'; -import { u32, u64 } from '../src/numbers'; -import { scalarEnum } from '../src/scalarEnum'; +import { scalarEnum, u32, u64 } from '../src'; +import { d, s } from './_helpers'; enum Empty {} enum Feedback { @@ -39,14 +38,14 @@ test('numerical enum (de)serialization', (t) => { // Invalid examples. t.throws(() => scalarEnum(Feedback).serialize('Missing'), { - message: (m: string) => - m.includes( - 'Invalid enum variant. Got "Missing", expected one of [0, 1, BAD, GOOD] ' + - 'or a number between 0 and 1' - ), + message: + 'Invalid scalar enum variant. ' + + 'Expected one of [0, 1, BAD, GOOD] or a number between 0 and 1, ' + + 'got "Missing".', }); t.throws(() => scalarEnum(Feedback).deserialize(new Uint8Array([2])), { - message: /Invalid enum variant\. Got "2"/, + message: + 'Enum discriminator out of range. Expected a number between 0 and 1, got 2.', }); }); @@ -81,15 +80,14 @@ test('lexical enum (de)serialization', (t) => { // Invalid examples. t.throws(() => scalarEnum(Direction).serialize('Diagonal' as any), { - message: (m: string) => - m.includes( - 'Invalid enum variant. Got "Diagonal", expected one of ' + - '[UP, DOWN, LEFT, RIGHT, Up, Down, Left, Right] ' + - 'or a number between 0 and 3' - ), + message: + 'Invalid scalar enum variant. ' + + 'Expected one of [UP, DOWN, LEFT, RIGHT, Up, Down, Left, Right] ' + + 'or a number between 0 and 3, got "Diagonal".', }); t.throws(() => scalarEnum(Direction).deserialize(new Uint8Array([4])), { - message: /Invalid enum variant\. Got "4"/, + message: + 'Enum discriminator out of range. Expected a number between 0 and 3, got 4.', }); }); diff --git a/packages/umi-serializer-data-view/test/set.test.ts b/packages/umi-serializers/test/set.test.ts similarity index 92% rename from packages/umi-serializer-data-view/test/set.test.ts rename to packages/umi-serializers/test/set.test.ts index 9d7bb635..45e5f3a7 100644 --- a/packages/umi-serializer-data-view/test/set.test.ts +++ b/packages/umi-serializers/test/set.test.ts @@ -1,9 +1,6 @@ import test from 'ava'; -import { Endian } from '@metaplex-foundation/umi'; -import { set } from '../src/set'; -import { s, d } from './_helpers'; -import { u16, u32, u64, u8 } from '../src/numbers'; -import { string } from '../src/string'; +import { Endian, set, string, u16, u32, u64, u8 } from '../src'; +import { d, s } from './_helpers'; test('prefixed (de)serialization', (t) => { // Empty. @@ -57,13 +54,13 @@ test('fixed (de)serialization', (t) => { // It fails if the set has a different size. t.throws(() => set(u8(), { size: 1 }).serialize(new Set()), { message: (m: string) => - m.includes('Expected set to have 1 items but got 0.'), + m.includes('Expected [set] to have 1 items, got 0.'), }); t.throws( () => set(string(), { size: 2 }).serialize(new Set(['a', 'b', 'c'])), { message: (m: string) => - m.includes('Expected set to have 2 items but got 3.'), + m.includes('Expected [set] to have 2 items, got 3.'), } ); }); diff --git a/packages/umi-serializer-data-view/test/string.test.ts b/packages/umi-serializers/test/string.test.ts similarity index 95% rename from packages/umi-serializer-data-view/test/string.test.ts rename to packages/umi-serializers/test/string.test.ts index 933ff810..ce07fd06 100644 --- a/packages/umi-serializer-data-view/test/string.test.ts +++ b/packages/umi-serializers/test/string.test.ts @@ -1,8 +1,6 @@ import test from 'ava'; -import { base16, base58, Endian } from '@metaplex-foundation/umi'; -import { s, d } from './_helpers'; -import { u16, u8 } from '../src/numbers'; -import { string } from '../src/string'; +import { Endian, base16, base58, string, u16, u8 } from '../src'; +import { d, s } from './_helpers'; test('prefixed (de)serialization', (t) => { // Empty string. diff --git a/packages/umi-serializer-data-view/test/struct.test.ts b/packages/umi-serializers/test/struct.test.ts similarity index 89% rename from packages/umi-serializer-data-view/test/struct.test.ts rename to packages/umi-serializers/test/struct.test.ts index a15803f3..f0918dad 100644 --- a/packages/umi-serializer-data-view/test/struct.test.ts +++ b/packages/umi-serializers/test/struct.test.ts @@ -1,9 +1,6 @@ import test from 'ava'; -import { option } from '../src/option'; -import { s, d } from './_helpers'; -import { u64, u8 } from '../src/numbers'; -import { string } from '../src/string'; -import { struct } from '../src/struct'; +import { option, string, struct, u64, u8 } from '../src'; +import { d, s } from './_helpers'; test('(de)serialization', (t) => { // Empty struct. diff --git a/packages/umi-serializers/test/tsconfig.json b/packages/umi-serializers/test/tsconfig.json new file mode 100644 index 00000000..2390f598 --- /dev/null +++ b/packages/umi-serializers/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./**/*"], + "compilerOptions": { + "module": "commonjs", + "outDir": "../dist/test", + "declarationDir": null, + "declaration": false, + "emitDeclarationOnly": false + } +} diff --git a/packages/umi-serializer-data-view/test/tuples.test.ts b/packages/umi-serializers/test/tuples.test.ts similarity index 90% rename from packages/umi-serializer-data-view/test/tuples.test.ts rename to packages/umi-serializers/test/tuples.test.ts index 3867bd61..0c50ed89 100644 --- a/packages/umi-serializer-data-view/test/tuples.test.ts +++ b/packages/umi-serializers/test/tuples.test.ts @@ -1,8 +1,6 @@ import test from 'ava'; -import { s, d } from './_helpers'; -import { i16, u64, u8 } from '../src/numbers'; -import { string } from '../src/string'; -import { tuple } from '../src/tuple'; +import { i16, string, tuple, u64, u8 } from '../src'; +import { d, s } from './_helpers'; test('serialization', (t) => { s(t, tuple([]), [], ''); diff --git a/packages/umi-serializer-data-view/test/unit.test.ts b/packages/umi-serializers/test/unit.test.ts similarity index 87% rename from packages/umi-serializer-data-view/test/unit.test.ts rename to packages/umi-serializers/test/unit.test.ts index 6be4ce82..69733762 100644 --- a/packages/umi-serializer-data-view/test/unit.test.ts +++ b/packages/umi-serializers/test/unit.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-void */ import test from 'ava'; -import { s, d } from './_helpers'; -import { unit } from '../src/unit'; +import { unit } from '../src'; +import { d, s } from './_helpers'; test('serialization', (t) => { s(t, unit(), undefined, ''); diff --git a/packages/umi-serializers/tsconfig.json b/packages/umi-serializers/tsconfig.json new file mode 100644 index 00000000..89a681d0 --- /dev/null +++ b/packages/umi-serializers/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist/esm", + "declarationDir": "dist/types" + } +} diff --git a/packages/umi-transaction-factory-web3js/package.json b/packages/umi-transaction-factory-web3js/package.json index 0d9d8104..b9814e51 100644 --- a/packages/umi-transaction-factory-web3js/package.json +++ b/packages/umi-transaction-factory-web3js/package.json @@ -27,8 +27,7 @@ "test": "ava" }, "dependencies": { - "@metaplex-foundation/umi-web3js-adapters": "workspace:^", - "@metaplex-foundation/umi-serializer-data-view": "workspace:^" + "@metaplex-foundation/umi-web3js-adapters": "workspace:^" }, "peerDependencies": { "@metaplex-foundation/umi": "workspace:^", diff --git a/packages/umi-transaction-factory-web3js/rollup.config.js b/packages/umi-transaction-factory-web3js/rollup.config.js index ba38fd10..98c85ced 100644 --- a/packages/umi-transaction-factory-web3js/rollup.config.js +++ b/packages/umi-transaction-factory-web3js/rollup.config.js @@ -3,6 +3,7 @@ import pkg from './package.json'; export default createConfigs({ pkg, + additionalExternals: ['@metaplex-foundation/umi/serializers'], builds: [ { dir: 'dist/esm', diff --git a/packages/umi-transaction-factory-web3js/src/createWeb3JsTransactionFactory.ts b/packages/umi-transaction-factory-web3js/src/createWeb3JsTransactionFactory.ts index 8bc016e9..390a2646 100644 --- a/packages/umi-transaction-factory-web3js/src/createWeb3JsTransactionFactory.ts +++ b/packages/umi-transaction-factory-web3js/src/createWeb3JsTransactionFactory.ts @@ -1,14 +1,10 @@ /* eslint-disable no-bitwise */ import { - base58, CompiledAddressLookupTable, CompiledInstruction, - Context, - mapSerializer, SdkError, SerializedTransaction, SerializedTransactionMessage, - Serializer, Transaction, TransactionFactoryInterface, TransactionInput, @@ -16,7 +12,18 @@ import { TransactionMessageHeader, TransactionVersion, } from '@metaplex-foundation/umi'; -import { shortU16 } from '@metaplex-foundation/umi-serializer-data-view'; +import { + shortU16, + base58, + Serializer, + mapSerializer, + struct, + bytes, + array, + string, + publicKey, + u8, +} from '@metaplex-foundation/umi/serializers'; import { fromWeb3JsMessage, toWeb3JsMessageFromInput, @@ -26,9 +33,7 @@ import { VersionedTransaction as Web3JsTransaction } from '@solana/web3.js'; const TRANSACTION_VERSION_FLAG = 0x80; const TRANSACTION_VERSION_MASK = 0x7f; -export function createWeb3JsTransactionFactory( - context: Pick -): TransactionFactoryInterface { +export function createWeb3JsTransactionFactory(): TransactionFactoryInterface { const create = (input: TransactionInput): Transaction => { const web3JsMessage = toWeb3JsMessageFromInput(input); const message = fromWeb3JsMessage(web3JsMessage); @@ -61,23 +66,20 @@ export function createWeb3JsTransactionFactory( ): TransactionMessage => getTransactionMessageSerializer().deserialize(serializedMessage)[0]; - const getTransactionSerializer = (): Serializer => { - const s = context.serializer; - return { - ...mapSerializer( - s.struct>([ - ['signatures', s.array(s.bytes({ size: 64 }), { size: shortU16() })], - ['serializedMessage', s.bytes()], - ]), - (value: Transaction): Omit => value, - (value: Omit): Transaction => ({ - ...value, - message: deserializeMessage(value.serializedMessage), - }) - ), - description: 'Transaction', - }; - }; + const getTransactionSerializer = (): Serializer => ({ + ...mapSerializer( + struct>([ + ['signatures', array(bytes({ size: 64 }), { size: shortU16() })], + ['serializedMessage', bytes()], + ]), + (value: Transaction): Omit => value, + (value: Omit): Transaction => ({ + ...value, + message: deserializeMessage(value.serializedMessage), + }) + ), + description: 'Transaction', + }); const getTransactionMessageSerializer = (): Serializer => ({ @@ -105,25 +107,23 @@ export function createWeb3JsTransactionFactory( const getTransactionMessageSerializerForVersion = ( version: TransactionVersion - ): Serializer => { - const s = context.serializer; - return s.struct([ + ): Serializer => + struct([ ['version', getTransactionVersionSerializer()], ['header', getTransactionMessageHeaderSerializer()], - ['accounts', s.array(s.publicKey(), { size: shortU16() })], - ['blockhash', s.string({ encoding: base58, size: 32 })], + ['accounts', array(publicKey(), { size: shortU16() })], + ['blockhash', string({ encoding: base58, size: 32 })], [ 'instructions', - s.array(getCompiledInstructionSerializer(), { size: shortU16() }), + array(getCompiledInstructionSerializer(), { size: shortU16() }), ], [ 'addressLookupTables', - s.array(getCompiledAddressLookupTableSerializer(), { + array(getCompiledAddressLookupTableSerializer(), { size: version === 'legacy' ? 0 : shortU16(), }), ], ]); - }; const getTransactionVersionSerializer = (): Serializer => ({ @@ -151,34 +151,28 @@ export function createWeb3JsTransactionFactory( }); const getTransactionMessageHeaderSerializer = - (): Serializer => { - const s = context.serializer; - return s.struct([ - ['numRequiredSignatures', s.u8()], - ['numReadonlySignedAccounts', s.u8()], - ['numReadonlyUnsignedAccounts', s.u8()], + (): Serializer => + struct([ + ['numRequiredSignatures', u8()], + ['numReadonlySignedAccounts', u8()], + ['numReadonlyUnsignedAccounts', u8()], ]); - }; const getCompiledInstructionSerializer = - (): Serializer => { - const s = context.serializer; - return s.struct([ - ['programIndex', s.u8()], - ['accountIndexes', s.array(s.u8(), { size: shortU16() })], - ['data', s.bytes({ size: shortU16() })], + (): Serializer => + struct([ + ['programIndex', u8()], + ['accountIndexes', array(u8(), { size: shortU16() })], + ['data', bytes({ size: shortU16() })], ]); - }; const getCompiledAddressLookupTableSerializer = - (): Serializer => { - const s = context.serializer; - return s.struct([ - ['publicKey', s.publicKey()], - ['writableIndexes', s.array(s.u8(), { size: shortU16() })], - ['readonlyIndexes', s.array(s.u8(), { size: shortU16() })], + (): Serializer => + struct([ + ['publicKey', publicKey()], + ['writableIndexes', array(u8(), { size: shortU16() })], + ['readonlyIndexes', array(u8(), { size: shortU16() })], ]); - }; return { create, diff --git a/packages/umi-transaction-factory-web3js/src/plugin.ts b/packages/umi-transaction-factory-web3js/src/plugin.ts index 154f0a58..4bbd3ffd 100644 --- a/packages/umi-transaction-factory-web3js/src/plugin.ts +++ b/packages/umi-transaction-factory-web3js/src/plugin.ts @@ -3,6 +3,6 @@ import { createWeb3JsTransactionFactory } from './createWeb3JsTransactionFactory export const web3JsTransactionFactory = (): UmiPlugin => ({ install(umi) { - umi.transactions = createWeb3JsTransactionFactory(umi); + umi.transactions = createWeb3JsTransactionFactory(); }, }); diff --git a/packages/umi-transaction-factory-web3js/test/_setup.ts b/packages/umi-transaction-factory-web3js/test/_setup.ts index 60440d07..1880dc4a 100644 --- a/packages/umi-transaction-factory-web3js/test/_setup.ts +++ b/packages/umi-transaction-factory-web3js/test/_setup.ts @@ -9,7 +9,6 @@ import { Umi, } from '@metaplex-foundation/umi'; import { web3JsEddsa } from '@metaplex-foundation/umi-eddsa-web3js'; -import { dataViewSerializer } from '@metaplex-foundation/umi-serializer-data-view'; import { fromWeb3JsInstruction, fromWeb3JsMessage, @@ -19,20 +18,17 @@ import { toWeb3JsTransaction, } from '@metaplex-foundation/umi-web3js-adapters'; import { - AddressLookupTableAccount as Web3JsAddressLookupTableAccount, - Message as Web3JsLegacyMessage, - MessageV0 as Web3JsV0Message, SystemProgram, + AddressLookupTableAccount as Web3JsAddressLookupTableAccount, TransactionInstruction as Web3JsInstruction, + Message as Web3JsLegacyMessage, VersionedTransaction as Web3JsTransaction, + MessageV0 as Web3JsV0Message, } from '@solana/web3.js'; import { web3JsTransactionFactory } from '../src'; export const createUmi = (): Umi => - baseCreateUmi() - .use(dataViewSerializer()) - .use(web3JsEddsa()) - .use(web3JsTransactionFactory()); + baseCreateUmi().use(web3JsEddsa()).use(web3JsTransactionFactory()); export const createTransferInstruction = ( umi: Umi diff --git a/packages/umi/package.json b/packages/umi/package.json index 20813902..6c834772 100644 --- a/packages/umi/package.json +++ b/packages/umi/package.json @@ -11,13 +11,19 @@ ".": { "import": "./dist/esm/index.mjs", "require": "./dist/cjs/index.cjs" + }, + "./serializers": { + "types": "./dist/types/serializers.d.ts", + "import": "./dist/esm/serializers.mjs", + "require": "./dist/cjs/serializers.cjs" } }, "files": [ "/dist/cjs", "/dist/esm", "/dist/types", - "/src" + "/src", + "*.d.ts" ], "scripts": { "lint": "eslint --ext js,ts,tsx src", @@ -26,6 +32,11 @@ "build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c", "test": "ava" }, + "dependencies": { + "@metaplex-foundation/umi-options": "workspace:^", + "@metaplex-foundation/umi-public-keys": "workspace:^", + "@metaplex-foundation/umi-serializers": "workspace:^" + }, "devDependencies": { "@ava/typescript": "^3.0.1", "ava": "^5.1.0" diff --git a/packages/umi/rollup.config.js b/packages/umi/rollup.config.js index ba38fd10..352dfb85 100644 --- a/packages/umi/rollup.config.js +++ b/packages/umi/rollup.config.js @@ -1,16 +1,35 @@ import { createConfigs } from '../../rollup.config'; import pkg from './package.json'; -export default createConfigs({ - pkg, - builds: [ - { - dir: 'dist/esm', - format: 'es', - }, - { - dir: 'dist/cjs', - format: 'cjs', - }, - ], -}); +export default [ + ...createConfigs({ + pkg, + builds: [ + { + dir: 'dist/esm', + format: 'es', + }, + { + dir: 'dist/cjs', + format: 'cjs', + }, + ], + }), + ...createConfigs({ + input: ['src/serializers.ts'], + dependenciesToExcludeInBundle: ['@metaplex-foundation/umi-serializers'], + pkg, + builds: [ + { + file: 'dist/esm/serializers.mjs', + bundle: true, + format: 'es', + }, + { + file: 'dist/cjs/serializers.cjs', + bundle: true, + format: 'cjs', + }, + ], + }), +]; diff --git a/packages/umi/serializers.d.ts b/packages/umi/serializers.d.ts new file mode 100644 index 00000000..7a81ec14 --- /dev/null +++ b/packages/umi/serializers.d.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/export +export * from './dist/types/serializers'; diff --git a/packages/umi/src/Account.ts b/packages/umi/src/Account.ts index 6fd2351b..380025c9 100644 --- a/packages/umi/src/Account.ts +++ b/packages/umi/src/Account.ts @@ -1,7 +1,7 @@ +import type { PublicKey } from '@metaplex-foundation/umi-public-keys'; +import type { Serializer } from '@metaplex-foundation/umi-serializers'; import type { SolAmount } from './Amount'; import { AccountNotFoundError, UnexpectedAccountError } from './errors'; -import type { PublicKey } from './PublicKey'; -import type { Serializer } from './Serializer'; /** * The size of an account header in bytes. diff --git a/packages/umi/src/Amount.ts b/packages/umi/src/Amount.ts index 842afe18..9d537792 100644 --- a/packages/umi/src/Amount.ts +++ b/packages/umi/src/Amount.ts @@ -1,6 +1,10 @@ +import { + NumberSerializer, + Serializer, + mapSerializer, +} from '@metaplex-foundation/umi-serializers'; import { BigIntInput, createBigInt } from './BigInt'; import { AmountMismatchError, UnexpectedAmountError } from './errors'; -import { mapSerializer, NumberSerializer, Serializer } from './Serializer'; /** * The identifier of an amount. diff --git a/packages/umi/src/Context.ts b/packages/umi/src/Context.ts index 5b64b533..161a0b52 100644 --- a/packages/umi/src/Context.ts +++ b/packages/umi/src/Context.ts @@ -42,7 +42,11 @@ export interface Context { programs: ProgramRepositoryInterface; /** An interface for sending RPC requests. */ rpc: RpcInterface; - /** An interface for serializing various types. */ + /** + * An interface for serializing various types. + * @deprecated This interface is deprecated. + * You can now directly use `@metaplex-foundation/umi/serializers` instead. + */ serializer: SerializerInterface; /** An interface for managing transactions. */ transactions: TransactionFactoryInterface; diff --git a/packages/umi/src/DateTime.ts b/packages/umi/src/DateTime.ts index 8660e0ee..299a020a 100644 --- a/packages/umi/src/DateTime.ts +++ b/packages/umi/src/DateTime.ts @@ -1,5 +1,9 @@ +import { + NumberSerializer, + Serializer, + mapSerializer, +} from '@metaplex-foundation/umi-serializers'; import { BigIntInput, createBigInt } from './BigInt'; -import { mapSerializer, NumberSerializer, Serializer } from './Serializer'; /** * Defines a string that can be parsed into a Date object. diff --git a/packages/umi/src/EddsaInterface.ts b/packages/umi/src/EddsaInterface.ts index b13bbc47..65332947 100644 --- a/packages/umi/src/EddsaInterface.ts +++ b/packages/umi/src/EddsaInterface.ts @@ -1,6 +1,6 @@ +import type { Pda, PublicKey } from '@metaplex-foundation/umi-public-keys'; import { InterfaceImplementationMissingError } from './errors'; import type { Keypair } from './Keypair'; -import type { Pda, PublicKey } from './PublicKey'; /** * Defines the interface for the EdDSA cryptography algorithm. diff --git a/packages/umi/src/GenericFile.ts b/packages/umi/src/GenericFile.ts index 0f2884f7..266bdcdf 100644 --- a/packages/umi/src/GenericFile.ts +++ b/packages/umi/src/GenericFile.ts @@ -1,4 +1,5 @@ -import { generateRandomString, utf8 } from './utils'; +import { utf8 } from '@metaplex-foundation/umi-serializers'; +import { generateRandomString } from './utils'; /** * A generic definition of a File represented as a buffer with diff --git a/packages/umi/src/GpaBuilder.ts b/packages/umi/src/GpaBuilder.ts index 41c1b3e5..2ee02e7a 100644 --- a/packages/umi/src/GpaBuilder.ts +++ b/packages/umi/src/GpaBuilder.ts @@ -1,15 +1,17 @@ +import { publicKey, PublicKey } from '@metaplex-foundation/umi-public-keys'; +import type { + Serializer, + StructToSerializerTuple, +} from '@metaplex-foundation/umi-serializers'; +import { base10, base58, base64 } from '@metaplex-foundation/umi-serializers'; import type { RpcAccount } from './Account'; import type { Context } from './Context'; import { SdkError } from './errors'; -import { publicKey, PublicKey } from './PublicKey'; import type { RpcDataFilter, RpcDataSlice, RpcGetProgramAccountsOptions, } from './RpcInterface'; -import type { Serializer } from './Serializer'; -import type { StructToSerializerTuple } from './SerializerInterface'; -import { base10, base58, base64 } from './utils'; /** * Callback for sorting raw accounts from `getProgramAccounts` RPC requests. diff --git a/packages/umi/src/Instruction.ts b/packages/umi/src/Instruction.ts index de8ce770..9a687fd8 100644 --- a/packages/umi/src/Instruction.ts +++ b/packages/umi/src/Instruction.ts @@ -1,4 +1,4 @@ -import type { PublicKey } from './PublicKey'; +import type { PublicKey } from '@metaplex-foundation/umi-public-keys'; import type { Signer } from './Signer'; /** diff --git a/packages/umi/src/Keypair.ts b/packages/umi/src/Keypair.ts index e48478d2..5d5a34f4 100644 --- a/packages/umi/src/Keypair.ts +++ b/packages/umi/src/Keypair.ts @@ -1,5 +1,5 @@ +import type { PublicKey } from '@metaplex-foundation/umi-public-keys'; import type { Context } from './Context'; -import type { PublicKey } from './PublicKey'; import type { Signer } from './Signer'; import { addTransactionSignature, Transaction } from './Transaction'; diff --git a/packages/umi/src/Option.ts b/packages/umi/src/Option.ts deleted file mode 100644 index a4e2dace..00000000 --- a/packages/umi/src/Option.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Defines a type `T` that can also be `null`. - * @category Utils — Options - */ -export type Nullable = T | null; - -/** - * An implementation of the Rust Option type in JavaScript. - * It can be one of the following: - * - {@link Some}: Meaning there is a value of type T. - * - {@link None}: Meaning there is no value. - * - * @category Utils — Options - */ -export type Option = Some | None; - -/** - * Represents an option of type `T` that has a value. - * - * @see {@link Option} - * @category Utils — Options - */ -export type Some = { __option: 'Some'; value: T }; - -/** - * Represents an option of type `T` that has no value. - * - * @see {@link Option} - * @category Utils — Options - */ -export type None = { __option: 'None' }; - -/** - * Creates a new {@link Option} of type `T` that has a value. - * - * @see {@link Option} - * @category Utils — Options - */ -export const some = (value: T): Option => ({ __option: 'Some', value }); - -/** - * Creates a new {@link Option} of type `T` that has no value. - * - * @see {@link Option} - * @category Utils — Options - */ -export const none = (): Option => ({ __option: 'None' }); - -/** - * Whether the given data is an {@link Option}. - * @category Utils — Options - */ -export const isOption = (input: any): input is Option => - input && - typeof input === 'object' && - '__option' in input && - ((input.__option === 'Some' && 'value' in input) || - input.__option === 'None'); - -/** - * Whether the given {@link Option} is a {@link Some}. - * @category Utils — Options - */ -export const isSome = (option: Option): option is Some => - option.__option === 'Some'; - -/** - * Whether the given {@link Option} is a {@link None}. - * @category Utils — Options - */ -export const isNone = (option: Option): option is None => - option.__option === 'None'; - -/** - * Unwraps the value of an {@link Option} of type `T`. - * If the option is a {@link Some}, it returns its value, - * Otherwise, it returns `null`. - * - * @category Utils — Options - * @deprecated Use {@link unwrapOption} instead. - */ -export const unwrapSome = (option: Option): Nullable => - isSome(option) ? option.value : null; - -/** - * Unwraps the value of an {@link Option} of type `T` - * or returns a custom fallback value. - * If the option is a {@link Some}, it returns its value, - * Otherwise, it returns the return value of the provided fallback callback. - * - * @category Utils — Options - * @deprecated Use {@link unwrapOption} instead. - */ -export const unwrapSomeOrElse = ( - option: Option, - fallback: () => U -): T | U => (isSome(option) ? option.value : fallback()); - -/** - * Unwraps the value of an {@link Option} of type `T` - * or returns a fallback value that defaults to `null`. - * - * @category Utils — Options - */ -export function unwrapOption(option: Option): Nullable; -export function unwrapOption(option: Option, fallback: () => U): T | U; -export function unwrapOption( - option: Option, - fallback?: () => U -): T | U { - if (isSome(option)) return option.value; - return fallback ? fallback() : (null as U); -} - -/** - * A type that defines the recursive unwrapping of a type `T` - * such that all nested {@link Option} types are unwrapped. - * - * For each nested {@link Option} type, if the option is a {@link Some}, - * it returns the type of its value, otherwise, it returns the provided - * fallback type `U` which defaults to `null`. - * - * @category Utils — Options - */ -type UnwrappedOption = T extends Some - ? UnwrappedOption - : T extends None - ? U - : T extends object - ? { [key in keyof T]: UnwrappedOption } - : T extends Array - ? Array> - : T; - -/** - * Recursively go through a type `T`such that all - * nested {@link Option} types are unwrapped. - * - * For each nested {@link Option} type, if the option is a {@link Some}, - * it returns its value, otherwise, it returns the provided fallback value - * which defaults to `null`. - * - * @category Utils — Options - */ -export function unwrapOptionRecursively(input: T): UnwrappedOption; -export function unwrapOptionRecursively( - input: T, - fallback: () => U -): UnwrappedOption; -export function unwrapOptionRecursively( - input: T, - fallback?: () => U -): UnwrappedOption { - // Because null passes `typeof input === 'object'`. - if (!input) return input as UnwrappedOption; - const next = (x: X) => - (fallback - ? unwrapOptionRecursively(x, fallback) - : unwrapOptionRecursively(x)) as UnwrappedOption; - - // Handle Option. - if (isOption(input)) { - if (isSome(input)) return next(input.value) as UnwrappedOption; - return (fallback ? fallback() : null) as UnwrappedOption; - } - - // Walk. - if (Array.isArray(input)) { - return input.map(next) as UnwrappedOption; - } - if (typeof input === 'object') { - return Object.fromEntries( - Object.entries(input).map(([k, v]) => [k, next(v)]) - ) as UnwrappedOption; - } - return input as UnwrappedOption; -} - -/** - * Wraps a nullable value into an {@link Option}. - * - * @category Utils — Options - */ -export const wrapNullable = (nullable: Nullable): Option => - nullable !== null ? some(nullable) : none(); diff --git a/packages/umi/src/Program.ts b/packages/umi/src/Program.ts index 20de16ee..178374ed 100644 --- a/packages/umi/src/Program.ts +++ b/packages/umi/src/Program.ts @@ -1,6 +1,6 @@ +import type { PublicKey } from '@metaplex-foundation/umi-public-keys'; import type { Cluster } from './Cluster'; import type { ProgramError } from './errors'; -import type { PublicKey } from './PublicKey'; /** * An error that contains Program logs. diff --git a/packages/umi/src/ProgramRepositoryInterface.ts b/packages/umi/src/ProgramRepositoryInterface.ts index 43fadf94..cba8d597 100644 --- a/packages/umi/src/ProgramRepositoryInterface.ts +++ b/packages/umi/src/ProgramRepositoryInterface.ts @@ -1,8 +1,11 @@ +import type { + PublicKey, + PublicKeyInput, +} from '@metaplex-foundation/umi-public-keys'; import type { ClusterFilter } from './Cluster'; -import { InterfaceImplementationMissingError, ProgramError } from './errors'; import type { ErrorWithLogs, Program } from './Program'; -import type { PublicKey, PublicKeyInput } from './PublicKey'; import type { Transaction } from './Transaction'; +import { InterfaceImplementationMissingError, ProgramError } from './errors'; /** * Defines the interface for a program repository. diff --git a/packages/umi/src/RpcInterface.ts b/packages/umi/src/RpcInterface.ts index 52c2f056..ba14104f 100644 --- a/packages/umi/src/RpcInterface.ts +++ b/packages/umi/src/RpcInterface.ts @@ -1,10 +1,9 @@ +import type { PublicKey } from '@metaplex-foundation/umi-public-keys'; import type { MaybeRpcAccount, RpcAccount } from './Account'; import { SolAmount } from './Amount'; import type { Cluster } from './Cluster'; import { DateTime } from './DateTime'; -import { InterfaceImplementationMissingError } from './errors'; import type { GenericAbortSignal } from './GenericAbortSignal'; -import type { PublicKey } from './PublicKey'; import type { Blockhash, BlockhashWithExpiryBlockHeight, @@ -14,6 +13,7 @@ import type { TransactionStatus, TransactionWithMeta, } from './Transaction'; +import { InterfaceImplementationMissingError } from './errors'; /** * Defines the interface for an RPC client. diff --git a/packages/umi/src/Serializer.ts b/packages/umi/src/Serializer.ts deleted file mode 100644 index 24d5a5e6..00000000 --- a/packages/umi/src/Serializer.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { SdkError } from './errors'; -import { fixBytes, mergeBytes } from './utils'; - -/** - * An object that can serialize and deserialize a value to and from a `Uint8Array`. - * It supports serializing looser types than it deserializes for convenience. - * For example, a `bigint` serializer will always deserialize to a `bigint` - * but can be used to serialize a `number`. - * - * @typeParam From - The type of the value to serialize. - * @typeParam To - The type of the deserialized value. Defaults to `From`. - * - * @category Serializers - */ -export type Serializer = { - /** A description for the serializer. */ - description: string; - /** The fixed size of the serialized value in bytes, or `null` if it is variable. */ - fixedSize: number | null; - /** The maximum size a serialized value can be in bytes, or `null` if it is variable. */ - maxSize: number | null; - /** The function that serializes a value into bytes. */ - serialize: (value: From) => Uint8Array; - /** - * The function that deserializes a value from bytes. - * It returns the deserialized value and the number of bytes read. - */ - deserialize: (buffer: Uint8Array, offset?: number) => [To, number]; -}; - -/** - * Defines a serializer for numbers and bigints. - * @category Serializers - */ -export type NumberSerializer = - | Serializer - | Serializer; - -/** - * Wraps all the attributes of an object in serializers. - * @category Serializers - */ -export type WrapInSerializer = { - [P in keyof T]: Serializer; -}; - -/** - * Converts a serializer A to a serializer B by mapping their values. - * @category Serializers - */ -export function mapSerializer( - serializer: Serializer, - unmap: (value: NewFrom) => OldFrom -): Serializer; -export function mapSerializer< - NewFrom, - OldFrom, - NewTo extends NewFrom = NewFrom, - OldTo extends OldFrom = OldFrom ->( - serializer: Serializer, - unmap: (value: NewFrom) => OldFrom, - map: (value: OldTo, buffer: Uint8Array, offset: number) => NewTo -): Serializer; -export function mapSerializer< - NewFrom, - OldFrom, - NewTo extends NewFrom = NewFrom, - OldTo extends OldFrom = OldFrom ->( - serializer: Serializer, - unmap: (value: NewFrom) => OldFrom, - map?: (value: OldTo, buffer: Uint8Array, offset: number) => NewTo -): Serializer { - return { - description: serializer.description, - fixedSize: serializer.fixedSize, - maxSize: serializer.maxSize, - serialize: (value: NewFrom) => serializer.serialize(unmap(value)), - deserialize: (buffer: Uint8Array, offset = 0) => { - const [value, length] = serializer.deserialize(buffer, offset); - return map - ? [map(value, buffer, offset), length] - : [value as any, length]; - }, - }; -} - -/** - * Creates a fixed-size serializer from a given serializer. - * - * @param serializer - The serializer to wrap into a fixed-size serializer. - * @param fixedBytes - The fixed number of bytes to read. - * @param description - A custom description for the serializer. - * - * @category Serializers - */ -export function fixSerializer( - serializer: Serializer, - fixedBytes: number, - description?: string -): Serializer { - return { - description: - description ?? `fixed(${fixedBytes}, ${serializer.description})`, - fixedSize: fixedBytes, - maxSize: fixedBytes, - serialize: (value: T) => fixBytes(serializer.serialize(value), fixedBytes), - deserialize: (buffer: Uint8Array, offset = 0) => { - // Slice the buffer to the fixed size. - buffer = buffer.slice(offset, offset + fixedBytes); - // Ensure we have enough bytes. - if (buffer.length < fixedBytes) { - throw new SdkError( - `Fixed serializer expected ${fixedBytes} bytes, got ${buffer.length}.` - ); - } - // If the nested serializer is fixed-size, pad and truncate the buffer accordingly. - if (serializer.fixedSize !== null) { - buffer = fixBytes(buffer, serializer.fixedSize); - } - // Deserialize the value using the nested serializer. - const [value] = serializer.deserialize(buffer, 0); - return [value, offset + fixedBytes]; - }, - }; -} - -/** - * Reverses the bytes of a fixed-size serializer. - * @category Serializers - */ -export function reverseSerializer( - serializer: Serializer -): Serializer { - if (serializer.fixedSize === null) { - throw new SdkError('Cannot reverse a serializer of variable size.'); - } - return { - ...serializer, - serialize: (value: T) => serializer.serialize(value).reverse(), - deserialize: (bytes: Uint8Array, offset = 0) => { - const fixedSize = serializer.fixedSize as number; - const newBytes = mergeBytes([ - bytes.slice(0, offset), - bytes.slice(offset, offset + fixedSize).reverse(), - bytes.slice(offset + fixedSize), - ]); - return serializer.deserialize(newBytes, offset); - }, - }; -} diff --git a/packages/umi/src/SerializerInterface.ts b/packages/umi/src/SerializerInterface.ts index a73d94eb..fcfcd30d 100644 --- a/packages/umi/src/SerializerInterface.ts +++ b/packages/umi/src/SerializerInterface.ts @@ -1,18 +1,40 @@ -import { DataEnum, ScalarEnum } from './Enums'; -import { InterfaceImplementationMissingError } from './errors'; -import type { PublicKey, PublicKeyInput } from './PublicKey'; +import type { Nullable, Option } from '@metaplex-foundation/umi-options'; import type { - NumberSerializer, + PublicKey, + PublicKeyInput, +} from '@metaplex-foundation/umi-public-keys'; +import type { + ArraySerializerOptions, + BoolSerializerOptions, + BytesSerializerOptions, + DataEnumSerializerOptions, + DataEnumToSerializerTuple, + MapSerializerOptions, + NullableSerializerOptions, + NumberSerializerOptions, + OptionSerializerOptions, + PublicKeySerializerOptions, + ScalarEnumSerializerOptions, Serializer, + SetSerializerOptions, + SingleByteNumberSerializerOptions, + StringSerializerOptions, + StructSerializerOptions, + StructToSerializerTuple, + TupleSerializerOptions, + UnitSerializerOptions, WrapInSerializer, -} from './Serializer'; -import type { Nullable, Option } from './Option'; +} from '@metaplex-foundation/umi-serializers'; +import { DataEnum, ScalarEnum } from './Enums'; +import { InterfaceImplementationMissingError } from './errors'; /** * Defines the interface for a set of serializers * that can be used to serialize/deserialize any Serde types. * * @category Context and Interfaces + * @deprecated This interface is deprecated. + * You can now directly use `@metaplex-foundation/umi/serializers` instead. */ export interface SerializerInterface { /** @@ -102,7 +124,7 @@ export interface SerializerInterface { */ enum( constructor: ScalarEnum & {}, - options?: EnumSerializerOptions + options?: ScalarEnumSerializerOptions ): Serializer; /** @@ -246,255 +268,6 @@ export interface SerializerInterface { ) => Serializer; } -/** - * Get the name and serializer of each field in a struct. - * @category Serializers - */ -export type StructToSerializerTuple = Array< - { - [K in keyof T]: [K, Serializer]; - }[keyof T] ->; - -/** - * Get the name and serializer of each variant in a data enum. - * @category Serializers - */ -export type DataEnumToSerializerTuple = Array< - T extends any - ? [ - T['__kind'], - keyof Omit extends never - ? Serializer, Omit> | Serializer - : Serializer, Omit> - ] - : never ->; - -/** - * Defines the endianness of a number serializer. - * @category Serializers - */ -export enum Endian { - Little = 'le', - Big = 'be', -} - -/** - * Represents all the size options for array-like serializers - * — i.e. `array`, `map` and `set`. - * - * It can be one of the following: - * - a {@link NumberSerializer} that prefixes its content with its size. - * - a fixed number of items. - * - or `'remainder'` to infer the number of items by dividing - * the rest of the buffer by the fixed size of its item. - * Note that this option is only available for fixed-size items. - * - * @category Serializers - */ -export type ArrayLikeSerializerSize = NumberSerializer | number | 'remainder'; - -/** - * Defines the common options for all methods in the serializer interface. - * @category Serializers - */ -export type BaseSerializerOptions = { - /** A custom description for the serializer. */ - description?: string; -}; - -/** - * Defines the options for tuple serializers. - * @category Serializers - */ -export type TupleSerializerOptions = BaseSerializerOptions; - -/** - * Defines the options for array serializers. - * @category Serializers - */ -export type ArraySerializerOptions = BaseSerializerOptions & { - /** - * The size of the array. - * @defaultValue `u32()` - */ - size?: ArrayLikeSerializerSize; -}; - -/** - * Defines the options for `Map` serializers. - * @category Serializers - */ -export type MapSerializerOptions = BaseSerializerOptions & { - /** - * The size of the map. - * @defaultValue `u32()` - */ - size?: ArrayLikeSerializerSize; -}; - -/** - * Defines the options for `Set` serializers. - * @category Serializers - */ -export type SetSerializerOptions = BaseSerializerOptions & { - /** - * The size of the set. - * @defaultValue `u32()` - */ - size?: ArrayLikeSerializerSize; -}; - -/** - * Defines the options for `Option` serializers. - * @category Serializers - */ -export type OptionSerializerOptions = BaseSerializerOptions & { - /** - * The serializer to use for the boolean prefix. - * @defaultValue `u8()` - */ - prefix?: NumberSerializer; - /** - * Whether the item serializer should be of fixed size. - * - * When this is true, a `None` value will skip the bytes that would - * have been used for the item. Note that this will only work if the - * item serializer is of fixed size. - * @defaultValue `false` - */ - fixed?: boolean; -}; - -/** - * Defines the options for `Nullable` serializers. - * @category Serializers - */ -export type NullableSerializerOptions = BaseSerializerOptions & { - /** - * The serializer to use for the boolean prefix. - * @defaultValue `u8()` - */ - prefix?: NumberSerializer; - /** - * Whether the item serializer should be of fixed size. - * - * When this is true, a `null` value will skip the bytes that would - * have been used for the item. Note that this will only work if the - * item serializer is of fixed size. - * @defaultValue `false` - */ - fixed?: boolean; -}; - -/** - * Defines the options for struct serializers. - * @category Serializers - */ -export type StructSerializerOptions = BaseSerializerOptions; - -/** - * Defines the options for scalar enum serializers. - * @category Serializers - */ -export type EnumSerializerOptions = BaseSerializerOptions & { - /** - * The serializer to use for the enum discriminator. - * @defaultValue `u8()` - */ - size?: NumberSerializer; -}; - -/** - * Defines the options for data enum serializers. - * @category Serializers - */ -export type DataEnumSerializerOptions = BaseSerializerOptions & { - /** - * The serializer to use for the enum discriminator prefixing the variant. - * @defaultValue `u8()` - */ - size?: NumberSerializer; -}; - -/** - * Defines the options for string serializers. - * @category Serializers - */ -export type StringSerializerOptions = BaseSerializerOptions & { - /** - * The size of the string. It can be one of the following: - * - a {@link NumberSerializer} that prefixes the string with its size. - * - a fixed number of bytes. - * - or `'variable'` to use the rest of the buffer. - * @defaultValue `u32()` - */ - size?: NumberSerializer | number | 'variable'; - /** - * The string serializer to use for encoding and decoding the content. - * @defaultValue `utf8` - */ - encoding?: Serializer; -}; - -/** - * Defines the options for boolean serializers. - * @category Serializers - */ -export type BoolSerializerOptions = BaseSerializerOptions & { - /** - * The number serializer to delegate to. - * @defaultValue `u8()` - */ - size?: NumberSerializer; -}; - -/** - * Defines the options for unit serializers. - * @category Serializers - */ -export type UnitSerializerOptions = BaseSerializerOptions; - -/** - * Defines the options for u8 and i8 serializers. - * @category Serializers - */ -export type SingleByteNumberSerializerOptions = BaseSerializerOptions; - -/** - * Defines the options for number serializers that use more than one byte. - * @category Serializers - */ -export type NumberSerializerOptions = BaseSerializerOptions & { - /** - * Whether the serializer should use little-endian or big-endian encoding. - * @defaultValue `Endian.Little` - */ - endian?: Endian; -}; - -/** - * Defines the options for bytes serializers. - * @category Serializers - */ -export type BytesSerializerOptions = BaseSerializerOptions & { - /** - * The size of the buffer. It can be one of the following: - * - a {@link NumberSerializer} that prefixes the buffer with its size. - * - a fixed number of bytes. - * - or `'variable'` to use the rest of the buffer. - * @defaultValue `'variable'` - */ - size?: NumberSerializer | number | 'variable'; -}; - -/** - * Defines the options for `PublicKey` serializers. - * @category Serializers - */ -export type PublicKeySerializerOptions = BaseSerializerOptions; - /** * An implementation of the {@link SerializerInterface} that throws an error when called. * @category Serializers diff --git a/packages/umi/src/Signer.ts b/packages/umi/src/Signer.ts index 9e7fa32f..73d79adb 100644 --- a/packages/umi/src/Signer.ts +++ b/packages/umi/src/Signer.ts @@ -1,4 +1,7 @@ -import { PublicKey, PublicKeyInput } from './PublicKey'; +import { + PublicKey, + PublicKeyInput, +} from '@metaplex-foundation/umi-public-keys'; import { Transaction } from './Transaction'; import { uniqueBy } from './utils'; diff --git a/packages/umi/src/Transaction.ts b/packages/umi/src/Transaction.ts index 71583a50..7b6b33f9 100644 --- a/packages/umi/src/Transaction.ts +++ b/packages/umi/src/Transaction.ts @@ -1,6 +1,6 @@ +import { PublicKey } from '@metaplex-foundation/umi-public-keys'; import { Amount, SolAmount } from './Amount'; import type { Instruction } from './Instruction'; -import { PublicKey } from './PublicKey'; import type { Commitment } from './RpcInterface'; /** diff --git a/packages/umi/src/errors/AccountNotFoundError.ts b/packages/umi/src/errors/AccountNotFoundError.ts index 151b7d96..adc2123c 100644 --- a/packages/umi/src/errors/AccountNotFoundError.ts +++ b/packages/umi/src/errors/AccountNotFoundError.ts @@ -1,4 +1,4 @@ -import { PublicKey } from '../PublicKey'; +import { PublicKey } from '@metaplex-foundation/umi-public-keys'; import { SdkError } from './SdkError'; /** @category Errors */ diff --git a/packages/umi/src/errors/UnexpectedAccountError.ts b/packages/umi/src/errors/UnexpectedAccountError.ts index b629236e..c2e2aa82 100644 --- a/packages/umi/src/errors/UnexpectedAccountError.ts +++ b/packages/umi/src/errors/UnexpectedAccountError.ts @@ -1,4 +1,4 @@ -import { PublicKey } from '../PublicKey'; +import { PublicKey } from '@metaplex-foundation/umi-public-keys'; import { SdkError } from './SdkError'; /** @category Errors */ diff --git a/packages/umi/src/errors/index.ts b/packages/umi/src/errors/index.ts index 79b079e1..a79d3b7f 100644 --- a/packages/umi/src/errors/index.ts +++ b/packages/umi/src/errors/index.ts @@ -2,7 +2,6 @@ export * from './AccountNotFoundError'; export * from './AmountMismatchError'; export * from './InterfaceImplementationMissingError'; export * from './InvalidBaseStringError'; -export * from './InvalidPublicKeyError'; export * from './ProgramError'; export * from './SdkError'; export * from './UmiError'; diff --git a/packages/umi/src/index.ts b/packages/umi/src/index.ts index 088e1f95..9666c4a9 100644 --- a/packages/umi/src/index.ts +++ b/packages/umi/src/index.ts @@ -1,3 +1,10 @@ +export * from '@metaplex-foundation/umi-options'; +export * from '@metaplex-foundation/umi-public-keys'; + +// Deprecated exports. +// They should instead be imported from "@metaplex-foundation/umi/serializers". +export * from './serializersInternal'; + export * from './Account'; export * from './Amount'; export * from './BigInt'; @@ -6,7 +13,6 @@ export * from './Context'; export * from './DateTime'; export * from './DownloaderInterface'; export * from './EddsaInterface'; -export * from './Enums'; export * from './errors'; export * from './GenericAbortSignal'; export * from './GenericFile'; @@ -17,12 +23,9 @@ export * from './HttpRequest'; export * from './HttpResponse'; export * from './Instruction'; export * from './Keypair'; -export * from './Option'; export * from './Program'; export * from './ProgramRepositoryInterface'; -export * from './PublicKey'; export * from './RpcInterface'; -export * from './Serializer'; export * from './SerializerInterface'; export * from './Signer'; export * from './SignerPlugins'; diff --git a/packages/umi/src/serializers.ts b/packages/umi/src/serializers.ts new file mode 100644 index 00000000..fdcd8149 --- /dev/null +++ b/packages/umi/src/serializers.ts @@ -0,0 +1 @@ +export * from '@metaplex-foundation/umi-serializers'; diff --git a/packages/umi/src/serializersInternal.ts b/packages/umi/src/serializersInternal.ts new file mode 100644 index 00000000..9515c290 --- /dev/null +++ b/packages/umi/src/serializersInternal.ts @@ -0,0 +1,190 @@ +import { + Serializer as _Serializer, + NumberSerializer as _NumberSerializer, + WrapInSerializer as _WrapInSerializer, + mapSerializer as _mapSerializer, + fixSerializer as _fixSerializer, + reverseSerializer as _reverseSerializer, + mergeBytes as _mergeBytes, + padBytes as _padBytes, + fixBytes as _fixBytes, + utf8 as _utf8, + baseX as _baseX, + base10 as _base10, + base58 as _base58, + base64 as _base64, + base16 as _base16, + bitArray as _bitArray, + removeNullCharacters as _removeNullCharacters, + padNullCharacters as _padNullCharacters, + StructToSerializerTuple as _StructToSerializerTuple, + DataEnumToSerializerTuple as _DataEnumToSerializerTuple, + Endian as _Endian, + ArrayLikeSerializerSize as _ArrayLikeSerializerSize, + BaseSerializerOptions as _BaseSerializerOptions, + TupleSerializerOptions as _TupleSerializerOptions, + ArraySerializerOptions as _ArraySerializerOptions, + MapSerializerOptions as _MapSerializerOptions, + SetSerializerOptions as _SetSerializerOptions, + OptionSerializerOptions as _OptionSerializerOptions, + NullableSerializerOptions as _NullableSerializerOptions, + StructSerializerOptions as _StructSerializerOptions, + ScalarEnumSerializerOptions as _ScalarEnumSerializerOptions, + DataEnumSerializerOptions as _DataEnumSerializerOptions, + StringSerializerOptions as _StringSerializerOptions, + BoolSerializerOptions as _BoolSerializerOptions, + UnitSerializerOptions as _UnitSerializerOptions, + SingleByteNumberSerializerOptions as _SingleByteNumberSerializerOptions, + NumberSerializerOptions as _NumberSerializerOptions, + BytesSerializerOptions as _BytesSerializerOptions, + PublicKeySerializerOptions as _PublicKeySerializerOptions, + ScalarEnum as _ScalarEnum, + DataEnum as _DataEnum, + GetDataEnumKind as _GetDataEnumKind, + GetDataEnumKindContent as _GetDataEnumKindContent, +} from '@metaplex-foundation/umi-serializers'; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type Serializer = _Serializer; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type NumberSerializer = _NumberSerializer; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type WrapInSerializer = _WrapInSerializer< + From, + To +>; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const mapSerializer = _mapSerializer; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const fixSerializer = _fixSerializer; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const reverseSerializer = _reverseSerializer; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const mergeBytes = _mergeBytes; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const padBytes = _padBytes; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const fixBytes = _fixBytes; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const utf8 = _utf8; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const baseX = _baseX; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const base10 = _base10; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const base58 = _base58; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const base64 = _base64; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const base16 = _base16; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const bitArray = _bitArray; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const removeNullCharacters = _removeNullCharacters; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const padNullCharacters = _padNullCharacters; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type StructToSerializerTuple< + T extends object, + U extends T +> = _StructToSerializerTuple; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type DataEnumToSerializerTuple< + T extends _DataEnum, + U extends T +> = _DataEnumToSerializerTuple; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export const Endian = _Endian; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type ArrayLikeSerializerSize = _ArrayLikeSerializerSize; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type BaseSerializerOptions = _BaseSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type TupleSerializerOptions = _TupleSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type ArraySerializerOptions = _ArraySerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type MapSerializerOptions = _MapSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type SetSerializerOptions = _SetSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type OptionSerializerOptions = _OptionSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type NullableSerializerOptions = _NullableSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type StructSerializerOptions = _StructSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type EnumSerializerOptions = _ScalarEnumSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type DataEnumSerializerOptions = _DataEnumSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type StringSerializerOptions = _StringSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type BoolSerializerOptions = _BoolSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type UnitSerializerOptions = _UnitSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type SingleByteNumberSerializerOptions = + _SingleByteNumberSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type NumberSerializerOptions = _NumberSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type BytesSerializerOptions = _BytesSerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type PublicKeySerializerOptions = _PublicKeySerializerOptions; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type ScalarEnum = _ScalarEnum; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type DataEnum = _DataEnum; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type GetDataEnumKind< + T extends _DataEnum, + K extends T['__kind'] +> = _GetDataEnumKind; + +/** @deprecated import from "@metaplex-foundation/umi/serializers" instead. */ +export type GetDataEnumKindContent< + T extends _DataEnum, + K extends T['__kind'] +> = _GetDataEnumKindContent; diff --git a/packages/umi/src/utils/bytes.ts b/packages/umi/src/utils/bytes.ts deleted file mode 100644 index 3bb26861..00000000 --- a/packages/umi/src/utils/bytes.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* eslint-disable no-bitwise */ -import { SdkError } from '../errors/SdkError'; -import { InvalidBaseStringError } from '../errors/InvalidBaseStringError'; -import type { Serializer } from '../Serializer'; -import { removeNullCharacters } from './nullCharacters'; - -/** - * Concatenates an array of `Uint8Array`s into a single `Uint8Array`. - * @category Utils - */ -export const mergeBytes = (bytesArr: Uint8Array[]): Uint8Array => { - const totalLength = bytesArr.reduce((total, arr) => total + arr.length, 0); - const result = new Uint8Array(totalLength); - let offset = 0; - bytesArr.forEach((arr) => { - result.set(arr, offset); - offset += arr.length; - }); - return result; -}; - -/** - * Pads a `Uint8Array` with zeroes to the specified length. - * If the array is longer than the specified length, it is returned as-is. - * @category Utils - */ -export const padBytes = (bytes: Uint8Array, length: number): Uint8Array => { - if (bytes.length >= length) return bytes; - const paddedBytes = new Uint8Array(length).fill(0); - paddedBytes.set(bytes); - return paddedBytes; -}; - -/** - * Fixes a `Uint8Array` to the specified length. - * If the array is longer than the specified length, it is truncated. - * If the array is shorter than the specified length, it is padded with zeroes. - * @category Utils - */ -export const fixBytes = (bytes: Uint8Array, length: number): Uint8Array => - padBytes(bytes.slice(0, length), length); - -/** - * A string serializer that uses UTF-8 encoding - * using the native `TextEncoder` API. - * @category Serializers - */ -export const utf8: Serializer = { - description: 'utf8', - fixedSize: null, - maxSize: null, - serialize(value: string) { - return new TextEncoder().encode(value); - }, - deserialize(buffer, offset = 0) { - const value = new TextDecoder().decode(buffer.slice(offset)); - return [removeNullCharacters(value), buffer.length]; - }, -}; - -/** - * A string serializer that uses a custom alphabet. - * This can be used to create serializers for base58, base64, etc. - * @category Serializers - */ -export const baseX = (alphabet: string): Serializer => { - const base = alphabet.length; - const baseBigInt = BigInt(base); - return { - description: `base${base}`, - fixedSize: null, - maxSize: null, - serialize(value: string): Uint8Array { - // Check if the value is valid. - if (!value.match(new RegExp(`^[${alphabet}]*$`))) { - throw new InvalidBaseStringError(value, base); - } - if (value === '') return new Uint8Array(); - - // Handle leading zeroes. - const chars = [...value]; - let trailIndex = chars.findIndex((c) => c !== alphabet[0]); - trailIndex = trailIndex === -1 ? chars.length : trailIndex; - const leadingZeroes = Array(trailIndex).fill(0); - if (trailIndex === chars.length) return Uint8Array.from(leadingZeroes); - - // From baseX to base10. - const tailChars = chars.slice(trailIndex); - let base10Number = 0n; - let baseXPower = 1n; - for (let i = tailChars.length - 1; i >= 0; i -= 1) { - base10Number += baseXPower * BigInt(alphabet.indexOf(tailChars[i])); - baseXPower *= baseBigInt; - } - - // From base10 to bytes. - const tailBytes = []; - while (base10Number > 0n) { - tailBytes.unshift(Number(base10Number % 256n)); - base10Number /= 256n; - } - return Uint8Array.from(leadingZeroes.concat(tailBytes)); - }, - deserialize(buffer, offset = 0): [string, number] { - if (buffer.length === 0) return ['', 0]; - - // Handle leading zeroes. - const bytes = buffer.slice(offset); - let trailIndex = bytes.findIndex((n) => n !== 0); - trailIndex = trailIndex === -1 ? bytes.length : trailIndex; - const leadingZeroes = alphabet[0].repeat(trailIndex); - if (trailIndex === bytes.length) return [leadingZeroes, buffer.length]; - - // From bytes to base10. - let base10Number = bytes - .slice(trailIndex) - .reduce((sum, byte) => sum * 256n + BigInt(byte), 0n); - - // From base10 to baseX. - const tailChars = []; - while (base10Number > 0n) { - tailChars.unshift(alphabet[Number(base10Number % baseBigInt)]); - base10Number /= baseBigInt; - } - - return [leadingZeroes + tailChars.join(''), buffer.length]; - }, - }; -}; - -/** - * A string serializer that uses base10 encoding. - * @category Serializers - */ -export const base10: Serializer = baseX('0123456789'); - -/** - * A string serializer that uses base58 encoding. - * @category Serializers - */ -export const base58: Serializer = baseX( - '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -); - -/** - * A string serializer that uses base64 encoding. - * @category Serializers - */ -export const base64: Serializer = { - description: 'base64', - fixedSize: null, - maxSize: null, - serialize(value: string) { - try { - return new Uint8Array( - atob(value) - .split('') - .map((c) => c.charCodeAt(0)) - ); - } catch (e) { - throw new InvalidBaseStringError(value, 64, e as Error); - } - }, - deserialize(buffer, offset = 0) { - const slice = buffer.slice(offset); - const value = btoa(String.fromCharCode.apply(null, [...slice])); - return [value, buffer.length]; - }, -}; - -/** - * A string serializer that uses base16 encoding. - * @category Serializers - */ -export const base16: Serializer = { - description: 'base16', - fixedSize: null, - maxSize: null, - serialize(value: string) { - const lowercaseValue = value.toLowerCase(); - if (!lowercaseValue.match(/^[0123456789abcdef]*$/)) { - throw new InvalidBaseStringError(value, 16); - } - const matches = lowercaseValue.match(/.{1,2}/g); - return Uint8Array.from( - matches ? matches.map((byte: string) => parseInt(byte, 16)) : [] - ); - }, - deserialize(buffer, offset = 0) { - const value = buffer - .slice(offset) - .reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); - return [value, buffer.length]; - }, -}; - -/** - * An array of boolean serializer that - * converts booleans to bits and vice versa. - * @category Serializers - */ -export const bitArray = ( - size: number, - backward = false -): Serializer => { - const backwardSuffix = backward ? '; backward' : ''; - return { - description: `bitArray(${size}${backwardSuffix})`, - fixedSize: size, - maxSize: size, - serialize(value: boolean[]) { - const bytes: number[] = []; - - for (let i = 0; i < size; i += 1) { - let byte = 0; - for (let j = 0; j < 8; j += 1) { - const feature = Number(value[i * 8 + j] ?? 0); - byte |= feature << (backward ? j : 7 - j); - } - if (backward) { - bytes.unshift(byte); - } else { - bytes.push(byte); - } - } - - return new Uint8Array(bytes); - }, - deserialize(bytes, offset = 0) { - const booleans: boolean[] = []; - let slice = bytes.slice(offset, offset + size); - slice = backward ? slice.reverse() : slice; - if (slice.length !== size) { - throw new SdkError( - `Serializer [bitArray] expected ${size} bytes, got ${slice.length}.` - ); - } - - slice.forEach((byte) => { - for (let i = 0; i < 8; i += 1) { - if (backward) { - booleans.push(Boolean(byte & 1)); - byte >>= 1; - } else { - booleans.push(Boolean(byte & 0b1000_0000)); - byte <<= 1; - } - } - }); - - return [booleans, offset + size]; - }, - }; -}; diff --git a/packages/umi/src/utils/index.ts b/packages/umi/src/utils/index.ts index 08d769d6..defb3c56 100644 --- a/packages/umi/src/utils/index.ts +++ b/packages/umi/src/utils/index.ts @@ -1,4 +1,2 @@ export * from './arrays'; -export * from './bytes'; -export * from './nullCharacters'; export * from './randomStrings'; diff --git a/packages/umi/test/GpaBuilder.test.ts b/packages/umi/test/GpaBuilder.test.ts index d0883674..6dcc8d7b 100644 --- a/packages/umi/test/GpaBuilder.test.ts +++ b/packages/umi/test/GpaBuilder.test.ts @@ -1,14 +1,13 @@ import test from 'ava'; import { - base58, createNullContext, defaultPublicKey, GpaBuilder, gpaBuilder, publicKeyBytes, RpcAccount, - Serializer, } from '../src'; +import { base58, Serializer } from '../src/serializers'; test('it can add a data slice', (t) => { let builder = getTestGpaBuilder().slice(42, 10); diff --git a/packages/umi/test/_setup.ts b/packages/umi/test/_setup.ts index 00655367..ceb1ae25 100644 --- a/packages/umi/test/_setup.ts +++ b/packages/umi/test/_setup.ts @@ -1,16 +1,16 @@ /* eslint-disable import/no-extraneous-dependencies */ import { - base10, Context, - createUmi as baseCreateUmi, - generatedSignerIdentity, - generateSigner, PublicKey, - publicKey, Signer, Umi, WrappedInstruction, + createUmi as baseCreateUmi, + generateSigner, + generatedSignerIdentity, + publicKey, } from '../src'; +import { base10 } from '../src/serializers'; export const createUmi = (): Umi => baseCreateUmi().use(generatedSignerIdentity()); diff --git a/packages/umi/test/utils/bytes.test.ts b/packages/umi/test/utils/bytes.test.ts deleted file mode 100644 index 3f447b98..00000000 --- a/packages/umi/test/utils/bytes.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import test from 'ava'; -import { base10, base16, base58, base64, bitArray, utf8 } from '../../src'; - -test('it can serialize utf8 strings', (t) => { - t.deepEqual(utf8.serialize(''), new Uint8Array([])); - t.deepEqual(utf8.deserialize(new Uint8Array([])), ['', 0]); - - t.deepEqual(utf8.serialize('0'), new Uint8Array([48])); - t.deepEqual(utf8.deserialize(new Uint8Array([48])), ['0', 1]); - - t.deepEqual(utf8.serialize('ABC'), new Uint8Array([65, 66, 67])); - t.deepEqual(utf8.deserialize(new Uint8Array([65, 66, 67])), ['ABC', 3]); - - const serializedHelloWorld = new Uint8Array([ - 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, - ]); - t.deepEqual(utf8.serialize('Hello World!'), serializedHelloWorld); - t.deepEqual(utf8.deserialize(serializedHelloWorld), ['Hello World!', 12]); - - t.deepEqual(utf8.serialize('語'), new Uint8Array([232, 170, 158])); - t.deepEqual(utf8.deserialize(new Uint8Array([232, 170, 158])), ['語', 3]); -}); - -test('it can serialize base 10 strings', (t) => { - t.deepEqual(base10.serialize(''), new Uint8Array([])); - t.deepEqual(base10.deserialize(new Uint8Array([])), ['', 0]); - - t.deepEqual(base10.serialize('0'), new Uint8Array([0])); - t.deepEqual(base10.deserialize(new Uint8Array([0])), ['0', 1]); - - t.deepEqual(base10.serialize('000'), new Uint8Array([0, 0, 0])); - t.deepEqual(base10.deserialize(new Uint8Array([0, 0, 0])), ['000', 3]); - - t.deepEqual(base10.serialize('1'), new Uint8Array([1])); - t.deepEqual(base10.deserialize(new Uint8Array([1])), ['1', 1]); - - t.deepEqual(base10.serialize('42'), new Uint8Array([42])); - t.deepEqual(base10.deserialize(new Uint8Array([42])), ['42', 1]); - - t.deepEqual(base10.serialize('1024'), new Uint8Array([4, 0])); - t.deepEqual(base10.deserialize(new Uint8Array([4, 0])), ['1024', 2]); - - t.deepEqual(base10.serialize('65535'), new Uint8Array([255, 255])); - t.deepEqual(base10.deserialize(new Uint8Array([255, 255])), ['65535', 2]); - - t.throws(() => base10.serialize('INVALID_INPUT'), { - message: (m) => - m.includes('Expected a string of base 10, got [INVALID_INPUT].'), - }); -}); - -test('it can serialize base 16 strings', (t) => { - t.deepEqual(base16.serialize(''), new Uint8Array([])); - t.deepEqual(base16.deserialize(new Uint8Array([])), ['', 0]); - - t.deepEqual(base16.serialize('0'), new Uint8Array([0])); - t.deepEqual(base16.serialize('00'), new Uint8Array([0])); - t.deepEqual(base16.deserialize(new Uint8Array([0])), ['00', 1]); - - t.deepEqual(base16.serialize('1'), new Uint8Array([1])); - t.deepEqual(base16.serialize('01'), new Uint8Array([1])); - t.deepEqual(base16.deserialize(new Uint8Array([1])), ['01', 1]); - - t.deepEqual(base16.serialize('2a'), new Uint8Array([42])); - t.deepEqual(base16.deserialize(new Uint8Array([42])), ['2a', 1]); - - t.deepEqual(base16.serialize('0400'), new Uint8Array([4, 0])); - t.deepEqual(base16.deserialize(new Uint8Array([4, 0])), ['0400', 2]); - - t.deepEqual(base16.serialize('ffff'), new Uint8Array([255, 255])); - t.deepEqual(base16.deserialize(new Uint8Array([255, 255])), ['ffff', 2]); - - t.throws(() => base16.serialize('INVALID_INPUT'), { - message: (m) => - m.includes('Expected a string of base 16, got [INVALID_INPUT].'), - }); -}); - -test('it can serialize base 58 strings', (t) => { - t.deepEqual(base58.serialize(''), new Uint8Array([])); - t.deepEqual(base58.deserialize(new Uint8Array([])), ['', 0]); - - t.deepEqual(base58.serialize('1'), new Uint8Array([0])); - t.deepEqual(base58.deserialize(new Uint8Array([0])), ['1', 1]); - - t.deepEqual(base58.serialize('2'), new Uint8Array([1])); - t.deepEqual(base58.deserialize(new Uint8Array([1])), ['2', 1]); - - t.deepEqual(base58.serialize('11'), new Uint8Array([0, 0])); - t.deepEqual(base58.deserialize(new Uint8Array([0, 0])), ['11', 2]); - - const zeroes32 = new Uint8Array(Array(32).fill(0)); - t.deepEqual(base58.serialize('1'.repeat(32)), zeroes32); - t.deepEqual(base58.deserialize(zeroes32), ['1'.repeat(32), 32]); - - t.deepEqual(base58.serialize('j'), new Uint8Array([42])); - t.deepEqual(base58.deserialize(new Uint8Array([42])), ['j', 1]); - - t.deepEqual(base58.serialize('Jf'), new Uint8Array([4, 0])); - t.deepEqual(base58.deserialize(new Uint8Array([4, 0])), ['Jf', 2]); - - t.deepEqual(base58.serialize('LUv'), new Uint8Array([255, 255])); - t.deepEqual(base58.deserialize(new Uint8Array([255, 255])), ['LUv', 2]); - - const pubkey = 'LorisCg1FTs89a32VSrFskYDgiRbNQzct1WxyZb7nuA'; - const bytes = new Uint8Array([ - 5, 19, 4, 94, 5, 47, 73, 25, 182, 8, 150, 61, 231, 60, 102, 110, 6, 114, - 224, 110, 40, 20, 10, 184, 65, 191, 241, 204, 131, 161, 120, 181, - ]); - t.deepEqual(base58.serialize(pubkey), bytes); - t.deepEqual(base58.deserialize(bytes), [pubkey, 32]); - - t.throws(() => base58.serialize('INVALID_INPUT'), { - message: (m) => - m.includes('Expected a string of base 58, got [INVALID_INPUT].'), - }); -}); - -test('it can serialize base 64 strings', (t) => { - t.deepEqual(base64.serialize(''), new Uint8Array([])); - t.deepEqual(base64.deserialize(new Uint8Array([])), ['', 0]); - - t.deepEqual(base64.serialize('AA'), new Uint8Array([0])); - t.deepEqual(base64.serialize('AA=='), new Uint8Array([0])); - t.deepEqual(base64.deserialize(new Uint8Array([0])), ['AA==', 1]); - - t.deepEqual(base64.serialize('AQ=='), new Uint8Array([1])); - t.deepEqual(base64.deserialize(new Uint8Array([1])), ['AQ==', 1]); - - t.deepEqual(base64.serialize('Kg'), new Uint8Array([42])); - t.deepEqual(base64.deserialize(new Uint8Array([42])), ['Kg==', 1]); - - const sentence = 'TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu'; - const bytes = new Uint8Array([ - 77, 97, 110, 121, 32, 104, 97, 110, 100, 115, 32, 109, 97, 107, 101, 32, - 108, 105, 103, 104, 116, 32, 119, 111, 114, 107, 46, - ]); - t.deepEqual(base64.serialize(sentence), bytes); - t.deepEqual(base64.deserialize(bytes), [sentence, 27]); - - t.throws(() => base64.serialize('INVALID_INPUT'), { - message: (m) => - m.includes('Expected a string of base 64, got [INVALID_INPUT].'), - }); - - t.throws(() => base64.serialize('A'), { - message: (m) => m.includes('Expected a string of base 64, got [A].'), - }); - - const base64TokenData = - 'AShNrkm2joOHhfQnRCzfSbrtDUkUcJSS7PJryR4PPjsnyyIWxL0ESVFoE7QWBowtz2B/iTtUGdb2EEyKbLuN5gEAAAAAAAAAAQAAAGCtpnOhgF7t+dM8By+nG51mKI9Dgb0RtO/6xvPX1w52AgAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; - const base16TokenData = - '01284dae49b68e838785f427442cdf49baed0d4914709492ecf26bc91e0f3e3b27cb2216c4bd0449516813b416068c2dcf607f893b5419d6f6104c8a6cbb8de601000000000000000100000060ada673a1805eedf9d33c072fa71b9d66288f4381bd11b4effac6f3d7d70e76020000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; - - t.deepEqual( - base16.deserialize(base64.serialize(base64TokenData))[0], - base16TokenData - ); - t.deepEqual( - base64.deserialize(base16.serialize(base16TokenData))[0], - base64TokenData - ); -}); - -test('it can serialize bit arrays', (t) => { - // Helper method to create array of booleans and bytes - const a = (bits: string) => [...bits].map((bit) => bit === '1'); - const b = (hex: string) => base16.serialize(hex); - - // Single byte, all zeros. - t.deepEqual(bitArray(1).serialize(a('00000000')), b('00')); - t.deepEqual(bitArray(1).deserialize(b('00')), [a('00000000'), 1]); - t.deepEqual(bitArray(1).deserialize(b('ff00'), 1), [a('00000000'), 2]); - - // Single byte, all ones. - t.deepEqual(bitArray(1).serialize(a('11111111')), b('ff')); - t.deepEqual(bitArray(1).deserialize(b('ff')), [a('11111111'), 1]); - t.deepEqual(bitArray(1).deserialize(b('00ff'), 1), [a('11111111'), 2]); - - // Single byte, first 2 bits, forwards. - t.deepEqual(bitArray(1).serialize(a('11000000')), b('c0')); - t.deepEqual(bitArray(1).deserialize(b('c0')), [a('11000000'), 1]); - t.deepEqual(bitArray(1).deserialize(b('ffc0'), 1), [a('11000000'), 2]); - - // Single byte, first 2 bits, backwards. - t.deepEqual(bitArray(1, true).serialize(a('11000000')), b('03')); - t.deepEqual(bitArray(1, true).deserialize(b('03')), [a('11000000'), 1]); - t.deepEqual(bitArray(1, true).deserialize(b('ff03'), 1), [a('11000000'), 2]); - - // Multiple bytes, first 2 bits, forwards. - const bitsA = '110000000000000000000000'; - t.deepEqual(bitArray(3).serialize(a(bitsA)), b('c00000')); - t.deepEqual(bitArray(3).deserialize(b('c00000')), [a(bitsA), 3]); - t.deepEqual(bitArray(3).deserialize(b('ffc00000'), 1), [a(bitsA), 4]); - - // Multiple bytes, first 2 bits, backwards. - t.deepEqual(bitArray(3, true).serialize(a(bitsA)), b('000003')); - t.deepEqual(bitArray(3, true).deserialize(b('000003')), [a(bitsA), 3]); - t.deepEqual(bitArray(3, true).deserialize(b('ff000003'), 1), [a(bitsA), 4]); - - // Multiple bytes, first half bits, forwards. - const bitsB = '111111111111000000000000'; - t.deepEqual(bitArray(3).serialize(a(bitsB)), b('fff000')); - t.deepEqual(bitArray(3).deserialize(b('fff000')), [a(bitsB), 3]); - t.deepEqual(bitArray(3).deserialize(b('00fff000'), 1), [a(bitsB), 4]); - - // Multiple bytes, first half bits, backwards. - t.deepEqual(bitArray(3, true).serialize(a(bitsB)), b('000fff')); - t.deepEqual(bitArray(3, true).deserialize(b('000fff')), [a(bitsB), 3]); - t.deepEqual(bitArray(3, true).deserialize(b('ff000fff'), 1), [a(bitsB), 4]); - - // It pads missing boolean values with false. - t.deepEqual(bitArray(1).serialize(a('101')), b('a0')); - t.deepEqual(bitArray(1).deserialize(b('a0')), [a('10100000'), 1]); - - // It truncates array of booleans if it is too long. - t.deepEqual(bitArray(1).serialize(a('000000001')), b('00')); - t.deepEqual(bitArray(1).deserialize(b('00')), [a('00000000'), 1]); - - // It fails if the buffer is too short. - t.throws(() => bitArray(3).deserialize(b('ff')), { - message: (m) => m.includes('Serializer [bitArray] expected 3 bytes, got 1'), - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74b1fe79..30f721f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,16 @@ importers: version: 28.16.1 packages/umi: + dependencies: + '@metaplex-foundation/umi-options': + specifier: workspace:^ + version: link:../umi-options + '@metaplex-foundation/umi-public-keys': + specifier: workspace:^ + version: link:../umi-public-keys + '@metaplex-foundation/umi-serializers': + specifier: workspace:^ + version: link:../umi-serializers devDependencies: '@ava/typescript': specifier: ^3.0.1 @@ -251,6 +261,15 @@ importers: specifier: ^0.17.1 version: 0.17.1 + packages/umi-options: + devDependencies: + '@ava/typescript': + specifier: ^3.0.1 + version: 3.0.1 + ava: + specifier: ^5.1.0 + version: 5.1.0(@ava/typescript@3.0.1) + packages/umi-program-repository: devDependencies: '@ava/typescript': @@ -263,6 +282,19 @@ importers: specifier: ^5.1.0 version: 5.1.0(@ava/typescript@3.0.1) + packages/umi-public-keys: + dependencies: + '@metaplex-foundation/umi-serializers-encodings': + specifier: workspace:^ + version: link:../umi-serializers-encodings + devDependencies: + '@ava/typescript': + specifier: ^3.0.1 + version: 3.0.1 + ava: + specifier: ^5.1.0 + version: 5.1.0(@ava/typescript@3.0.1) + packages/umi-rpc-chunk-get-accounts: devDependencies: '@ava/typescript': @@ -331,6 +363,66 @@ importers: specifier: ^5.1.0 version: 5.1.0(@ava/typescript@3.0.1) + packages/umi-serializers: + dependencies: + '@metaplex-foundation/umi-options': + specifier: workspace:^ + version: link:../umi-options + '@metaplex-foundation/umi-public-keys': + specifier: workspace:^ + version: link:../umi-public-keys + '@metaplex-foundation/umi-serializers-core': + specifier: workspace:^ + version: link:../umi-serializers-core + '@metaplex-foundation/umi-serializers-encodings': + specifier: workspace:^ + version: link:../umi-serializers-encodings + '@metaplex-foundation/umi-serializers-numbers': + specifier: workspace:^ + version: link:../umi-serializers-numbers + devDependencies: + '@ava/typescript': + specifier: ^3.0.1 + version: 3.0.1 + ava: + specifier: ^5.1.0 + version: 5.1.0(@ava/typescript@3.0.1) + + packages/umi-serializers-core: + devDependencies: + '@ava/typescript': + specifier: ^3.0.1 + version: 3.0.1 + ava: + specifier: ^5.1.0 + version: 5.1.0(@ava/typescript@3.0.1) + + packages/umi-serializers-encodings: + dependencies: + '@metaplex-foundation/umi-serializers-core': + specifier: workspace:^ + version: link:../umi-serializers-core + devDependencies: + '@ava/typescript': + specifier: ^3.0.1 + version: 3.0.1 + ava: + specifier: ^5.1.0 + version: 5.1.0(@ava/typescript@3.0.1) + + packages/umi-serializers-numbers: + dependencies: + '@metaplex-foundation/umi-serializers-core': + specifier: workspace:^ + version: link:../umi-serializers-core + devDependencies: + '@ava/typescript': + specifier: ^3.0.1 + version: 3.0.1 + ava: + specifier: ^5.1.0 + version: 5.1.0(@ava/typescript@3.0.1) + packages/umi-signer-derived: dependencies: '@noble/hashes': @@ -393,9 +485,6 @@ importers: packages/umi-transaction-factory-web3js: dependencies: - '@metaplex-foundation/umi-serializer-data-view': - specifier: workspace:^ - version: link:../umi-serializer-data-view '@metaplex-foundation/umi-web3js-adapters': specifier: workspace:^ version: link:../umi-web3js-adapters diff --git a/rollup.config.js b/rollup.config.js index ddfca6f6..32cd057a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -23,6 +23,7 @@ const createConfig = (build, options) => { } = build; const { + input = ['src/index.ts'], pkg, extensions = ['.js', '.ts'], globals = {}, @@ -44,7 +45,7 @@ const createConfig = (build, options) => { const entryFileNames = `[name].${outputExtension}`; return { - input: ['src/index.ts'], + input, output: { dir, file,