From 93a633dfa76d8577b3cb6f5c1a906dfa3b9b63a2 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 4 Aug 2023 10:52:14 +0200 Subject: [PATCH] Major Update of Borsh (#65) * indexed by class name instead of the class itself * serializer 1.0 * Implemented deserializer * Fixed indentation * Added schema validation * minor improvements * added more tests * added more tests * added more tests * updated readme * minor fix to examples * bump in version * minor update to README.md * minor update to README.md * trigger actions * Removed unnecesary packages + fixed lint * simplified buffer * added base encode/decode * implemented enums and removed deserializing of classes * better organized testing * exported schema * Added forgotten schemas to schema type * allowing numbers in BN * schema now leads serialization order * bump version * feat: allow strings in BN * feat: more tests & checkSchema flag * fix: made compatible to ES5 * updated readme * feat: building cjs & esm * feat: cjs & esm working versions * removed BN.js & bs58 * simplified tests * small change in bigint method * added compatibility with BN --- .build_scripts/prepare-package-json.js | 29 ++ README.md | 91 +++- borsh-ts/.eslintrc.yml | 1 - borsh-ts/buffer.ts | 87 ++++ borsh-ts/deserialize.ts | 125 +++++ borsh-ts/index.ts | 487 +----------------- borsh-ts/serialize.ts | 177 +++++++ borsh-ts/test/(de)serialize.test.js | 121 +++++ borsh-ts/test/.eslintrc.yml | 7 + borsh-ts/test/fuzz/borsh-roundtrip.js | 19 - ...37181f6340b5b269b7faee05d97fe65b1ab91cd171 | Bin 160 -> 0 bytes ...06f0a57fc7b5f2cb5c08ba00ff900a79ebd0637da7 | Bin 163 -> 0 bytes ...b3ffb19fcaf35deef41f8c084726c5aac5a4551696 | Bin 168 -> 0 bytes ...195c308c8af1a2006a76ba8fcfdca5ea5105ec9db1 | Bin 158 -> 0 bytes ...28d3381c64c61654579dc455a9e5ccaa54bc093721 | Bin 160 -> 0 bytes ...4ba237896a8ee4dffebd27b65c8101a1d8897d6409 | Bin 164 -> 0 bytes ...a2de056c34c40850bbe61b9ebc82cf48365484f4fc | Bin 155 -> 0 bytes ...38baae9bddbd620d018f803540e09ac1bc82ec3a6c | Bin 155 -> 0 bytes ...58a5c55d25830a8efffb9e8661a81e2db3584ca512 | Bin 155 -> 0 bytes ...137cf3e506f550fcfa19363050d1e4917250d83d32 | Bin 168 -> 0 bytes ...5107e67373646fe7c5370030819ccc13990375ca73 | Bin 160 -> 0 bytes ...bdaf44b17f2cd52501f1b5567ed05406163cf96fef | Bin 155 -> 0 bytes ...11e27c119009d0fcccbdddd5fe669c8c02bf04f7dd | Bin 170 -> 0 bytes ...58136c1f2dbd9ed3cad450556faf7b18db4184b42e | Bin 155 -> 0 bytes ...6d6e8ee36819bb22b7304f4e23f61d189a6c221f92 | Bin 164 -> 0 bytes ...2a1be308f7d25d21f81622a7ffc7da3733c03eb572 | Bin 160 -> 0 bytes ...f8e5b7e1c9962a36c8489ada9cb0f798c1ec967eb7 | Bin 109 -> 0 bytes ...b510fffc51f5c78078db2a2fb5cf19b0bd840291ee | Bin 155 -> 0 bytes ...f841050f0b2d78fa519ac76f178db0757d1bd3e96e | Bin 174 -> 0 bytes ...f981ab86d16876888bb7ba665637c5717c0a8916b2 | Bin 155 -> 0 bytes .../test/fuzz/transaction-example/enums.d.ts | 7 - .../test/fuzz/transaction-example/enums.js | 22 - .../fuzz/transaction-example/key_pair.d.ts | 61 --- .../test/fuzz/transaction-example/key_pair.js | 125 ----- .../fuzz/transaction-example/serialize.d.ts | 41 -- .../fuzz/transaction-example/serialize.js | 16 - .../test/fuzz/transaction-example/signer.d.ts | 63 --- .../test/fuzz/transaction-example/signer.js | 82 --- .../fuzz/transaction-example/transaction.d.ts | 106 ---- .../fuzz/transaction-example/transaction.js | 234 --------- borsh-ts/test/serialize.test.js | 204 -------- borsh-ts/test/structures.js | 136 +++++ borsh-ts/test/utils.test.js | 44 ++ borsh-ts/types.ts | 16 + borsh-ts/utils.ts | 121 +++++ examples/cjs/index.js | 9 + examples/cjs/package.json | 10 + examples/cjs/tsconfig.json | 11 + examples/esm/index.js | 9 + examples/esm/package.json | 11 + examples/esm/tsconfig.json | 11 + lib/cjs/buffer.d.ts | 22 + lib/cjs/buffer.js | 69 +++ lib/cjs/deserialize.d.ts | 18 + lib/cjs/deserialize.js | 118 +++++ lib/cjs/index.d.ts | 4 + lib/cjs/index.js | 45 ++ lib/cjs/serialize.d.ts | 22 + lib/cjs/serialize.js | 186 +++++++ lib/cjs/types.d.ts | 32 ++ lib/cjs/types.js | 4 + lib/cjs/utils.d.ts | 10 + lib/cjs/utils.js | 134 +++++ lib/esm/buffer.d.ts | 22 + lib/esm/buffer.js | 66 +++ lib/esm/deserialize.d.ts | 18 + lib/esm/deserialize.js | 115 +++++ lib/esm/index.d.ts | 4 + lib/esm/index.js | 17 + lib/esm/package.json | 1 + lib/esm/serialize.d.ts | 22 + lib/esm/serialize.js | 160 ++++++ lib/esm/types.d.ts | 32 ++ lib/esm/types.js | 1 + lib/esm/utils.d.ts | 10 + lib/esm/utils.js | 125 +++++ lib/index.d.ts | 52 -- lib/index.js | 449 ---------------- lib/types/buffer.d.ts | 22 + lib/types/deserialize.d.ts | 18 + lib/types/index.d.ts | 4 + lib/types/serialize.d.ts | 22 + lib/types/types.d.ts | 32 ++ lib/types/utils.d.ts | 10 + package.json | 36 +- tsconfig.cjs.json | 7 + tsconfig.esm.json | 7 + tsconfig.json | 8 - tsconfig.types.json | 8 + 89 files changed, 2408 insertions(+), 2007 deletions(-) create mode 100644 .build_scripts/prepare-package-json.js create mode 100644 borsh-ts/buffer.ts create mode 100644 borsh-ts/deserialize.ts create mode 100644 borsh-ts/serialize.ts create mode 100644 borsh-ts/test/(de)serialize.test.js delete mode 100644 borsh-ts/test/fuzz/borsh-roundtrip.js delete mode 100644 borsh-ts/test/fuzz/corpus/004b705c22403d1c22ceab37181f6340b5b269b7faee05d97fe65b1ab91cd171 delete mode 100644 borsh-ts/test/fuzz/corpus/25b2da453a8cb0d574d31a06f0a57fc7b5f2cb5c08ba00ff900a79ebd0637da7 delete mode 100644 borsh-ts/test/fuzz/corpus/2f6f8117189cb82e111a13b3ffb19fcaf35deef41f8c084726c5aac5a4551696 delete mode 100644 borsh-ts/test/fuzz/corpus/4d97a7d362a8dc9da33040195c308c8af1a2006a76ba8fcfdca5ea5105ec9db1 delete mode 100644 borsh-ts/test/fuzz/corpus/4eb3f263ac0d9bac83d67e28d3381c64c61654579dc455a9e5ccaa54bc093721 delete mode 100644 borsh-ts/test/fuzz/corpus/5a0ac0f8936af102c2ea974ba237896a8ee4dffebd27b65c8101a1d8897d6409 delete mode 100644 borsh-ts/test/fuzz/corpus/67fc403d488908e0a27d74a2de056c34c40850bbe61b9ebc82cf48365484f4fc delete mode 100644 borsh-ts/test/fuzz/corpus/7fa19d2ffc291390ebe8a238baae9bddbd620d018f803540e09ac1bc82ec3a6c delete mode 100644 borsh-ts/test/fuzz/corpus/824d229c23c200a937bcd558a5c55d25830a8efffb9e8661a81e2db3584ca512 delete mode 100644 borsh-ts/test/fuzz/corpus/9d6a787b7d95c13bb67d02137cf3e506f550fcfa19363050d1e4917250d83d32 delete mode 100644 borsh-ts/test/fuzz/corpus/a330e2fd8d25278e3d28205107e67373646fe7c5370030819ccc13990375ca73 delete mode 100644 borsh-ts/test/fuzz/corpus/a53d5be4493c7cccab1d70bdaf44b17f2cd52501f1b5567ed05406163cf96fef delete mode 100644 borsh-ts/test/fuzz/corpus/b7361e727623b1cdafda1411e27c119009d0fcccbdddd5fe669c8c02bf04f7dd delete mode 100644 borsh-ts/test/fuzz/corpus/b98f80e8a618038000215158136c1f2dbd9ed3cad450556faf7b18db4184b42e delete mode 100644 borsh-ts/test/fuzz/corpus/bec080831059d96afd82176d6e8ee36819bb22b7304f4e23f61d189a6c221f92 delete mode 100644 borsh-ts/test/fuzz/corpus/cf94b13d93b7accf950d3d2a1be308f7d25d21f81622a7ffc7da3733c03eb572 delete mode 100644 borsh-ts/test/fuzz/corpus/d2897d4dc13f861fb5e7caf8e5b7e1c9962a36c8489ada9cb0f798c1ec967eb7 delete mode 100644 borsh-ts/test/fuzz/corpus/dfbc571f6a049f8f66f2fcb510fffc51f5c78078db2a2fb5cf19b0bd840291ee delete mode 100644 borsh-ts/test/fuzz/corpus/e08ccfae0c2fd6432c8e1bf841050f0b2d78fa519ac76f178db0757d1bd3e96e delete mode 100644 borsh-ts/test/fuzz/corpus/ecd71b60d1820dbbcb06d1f981ab86d16876888bb7ba665637c5717c0a8916b2 delete mode 100644 borsh-ts/test/fuzz/transaction-example/enums.d.ts delete mode 100644 borsh-ts/test/fuzz/transaction-example/enums.js delete mode 100644 borsh-ts/test/fuzz/transaction-example/key_pair.d.ts delete mode 100644 borsh-ts/test/fuzz/transaction-example/key_pair.js delete mode 100644 borsh-ts/test/fuzz/transaction-example/serialize.d.ts delete mode 100644 borsh-ts/test/fuzz/transaction-example/serialize.js delete mode 100644 borsh-ts/test/fuzz/transaction-example/signer.d.ts delete mode 100644 borsh-ts/test/fuzz/transaction-example/signer.js delete mode 100644 borsh-ts/test/fuzz/transaction-example/transaction.d.ts delete mode 100644 borsh-ts/test/fuzz/transaction-example/transaction.js delete mode 100644 borsh-ts/test/serialize.test.js create mode 100644 borsh-ts/test/structures.js create mode 100644 borsh-ts/test/utils.test.js create mode 100644 borsh-ts/types.ts create mode 100644 borsh-ts/utils.ts create mode 100644 examples/cjs/index.js create mode 100644 examples/cjs/package.json create mode 100644 examples/cjs/tsconfig.json create mode 100644 examples/esm/index.js create mode 100644 examples/esm/package.json create mode 100644 examples/esm/tsconfig.json create mode 100644 lib/cjs/buffer.d.ts create mode 100644 lib/cjs/buffer.js create mode 100644 lib/cjs/deserialize.d.ts create mode 100644 lib/cjs/deserialize.js create mode 100644 lib/cjs/index.d.ts create mode 100644 lib/cjs/index.js create mode 100644 lib/cjs/serialize.d.ts create mode 100644 lib/cjs/serialize.js create mode 100644 lib/cjs/types.d.ts create mode 100644 lib/cjs/types.js create mode 100644 lib/cjs/utils.d.ts create mode 100644 lib/cjs/utils.js create mode 100644 lib/esm/buffer.d.ts create mode 100644 lib/esm/buffer.js create mode 100644 lib/esm/deserialize.d.ts create mode 100644 lib/esm/deserialize.js create mode 100644 lib/esm/index.d.ts create mode 100644 lib/esm/index.js create mode 100644 lib/esm/package.json create mode 100644 lib/esm/serialize.d.ts create mode 100644 lib/esm/serialize.js create mode 100644 lib/esm/types.d.ts create mode 100644 lib/esm/types.js create mode 100644 lib/esm/utils.d.ts create mode 100644 lib/esm/utils.js delete mode 100644 lib/index.d.ts delete mode 100644 lib/index.js create mode 100644 lib/types/buffer.d.ts create mode 100644 lib/types/deserialize.d.ts create mode 100644 lib/types/index.d.ts create mode 100644 lib/types/serialize.d.ts create mode 100644 lib/types/types.d.ts create mode 100644 lib/types/utils.d.ts create mode 100644 tsconfig.cjs.json create mode 100644 tsconfig.esm.json create mode 100644 tsconfig.types.json diff --git a/.build_scripts/prepare-package-json.js b/.build_scripts/prepare-package-json.js new file mode 100644 index 000000000..398867fa8 --- /dev/null +++ b/.build_scripts/prepare-package-json.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const path = require('path'); + +const buildDir = './lib'; +function createEsmModulePackageJson() { + fs.readdir(buildDir, function (err, dirs) { + if (err) { + throw err; + } + dirs.forEach(function (dir) { + if (dir === 'esm') { + var packageJsonFile = path.join(buildDir, dir, '/package.json'); + if (!fs.existsSync(packageJsonFile)) { + fs.writeFile( + packageJsonFile, + new Uint8Array(Buffer.from('{"type": "module"}')), + function (err) { + if (err) { + throw err; + } + } + ); + } + } + }); + }); +} + +createEsmModulePackageJson(); diff --git a/README.md b/README.md index fc3beb234..3bcf97081 100644 --- a/README.md +++ b/README.md @@ -14,37 +14,76 @@ Borsh stands for _Binary Object Representation Serializer for Hashing_. It is me safety, speed, and comes with a strict specification. ## Examples -### Serializing an object + +### (De)serializing a Value ```javascript -const value = new Test({ x: 255, y: 20, z: '123', q: [1, 2, 3] }); -const schema = new Map([[Test, { kind: 'struct', fields: [['x', 'u8'], ['y', 'u64'], ['z', 'string'], ['q', [3]]] }]]); -const buffer = borsh.serialize(schema, value); +import * as borsh from 'borsh'; + +const encodedU16 = borsh.serialize('u16', 2); +const decodedU16 = borsh.deserialize('u16', encodedU16); + +const encodedStr = borsh.serialize('string', 'testing'); +const decodedStr = borsh.deserialize('string', encodedStr); ``` -### Deserializing an object +### (De)serializing an Object ```javascript -const newValue = borsh.deserialize(schema, Test, buffer); +import * as borsh from 'borsh'; + +const value = {x: 255, y: BigInt(20), z: '123', arr: [1, 2, 3]}; +const schema = { struct: { x: 'u8', y: 'u64', 'z': 'string', 'arr': { array: { type: 'u8' }}}}; + +const encoded = borsh.serialize(schema, value); +const decoded = borsh.deserialize(schema, encoded); ``` -## Type Mappings - -| Borsh | TypeScript | -|-----------------------|----------------| -| `u8` integer | `number` | -| `u16` integer | `number` | -| `u32` integer | `number` | -| `u64` integer | `BN` | -| `u128` integer | `BN` | -| `u256` integer | `BN` | -| `u512` integer | `BN` | -| `f32` float | N/A | -| `f64` float | N/A | -| fixed-size byte array | `Uint8Array` | -| UTF-8 string | `string` | -| option | `null` or type | -| map | N/A | -| set | N/A | -| structs | `any` | +## API +The package exposes the following functions: +- `serialize(schema: Schema, obj: any): Uint8Array` - serializes an object `obj` according to the schema `schema`. +- `deserialize(schema: Schema, buffer: Uint8Array, class?: Class): any` - deserializes an object according to the schema `schema` from the buffer `buffer`. If the optional parameter `class` is present, the deserialized object will be an of `class`. + +## Schemas +Schemas are used to describe the structure of the data being serialized or deserialized. They are used to +validate the data and to determine the order of the fields in the serialized data. + +> NOTE: You can find examples of valid in the [test](./borsh-ts/test/utils.test.js) folder. + +### Basic Types +Basic types are described by a string. The following types are supported: +- `u8`, `u16`, `u32`, `u64`, `u128` - unsigned integers of 8, 16, 32, 64, and 128 bits respectively. +- `i8`, `i16`, `i32`, `i64`, `i128` - signed integers of 8, 16, 32, 64, and 128 bits respectively. +- `f32`, `f64` - IEEE 754 floating point numbers of 32 and 64 bits respectively. +- `bool` - boolean value. +- `string` - UTF-8 string. + +### Arrays, Options, Maps, Sets, Enums, and Structs +More complex objects are described by a JSON object. The following types are supported: +- `{ array: { type: Schema, len?: number } }` - an array of objects of the same type. The type of the array elements is described by the `type` field. If the field `len` is present, the array is fixed-size and the length of the array is `len`. Otherwise, the array is dynamic-sized and the length of the array is serialized before the elements. +- `{ option: Schema }` - an optional object. The type of the object is described by the `type` field. +- `{ map: { key: Schema, value: Schema }}` - a map. The type of the keys and values are described by the `key` and `value` fields respectively. +- `{ set: Schema }` - a set. The type of the elements is described by the `type` field. +- `{ enum: [{ className1: { struct: {...} } }, { className2: { struct: {...} } }, ... ] }` - an enum. The variants of the enum are described by the `className1`, `className2`, etc. fields. The variants are structs. +- `{ struct: { field1: Schema1, field2: Schema2, ... } }` - a struct. The fields of the struct are described by the `field1`, `field2`, etc. fields. + +### Type Mappings + +| Javascript | Borsh | +|------------------|-----------------------------------| +| `number` | `u8` `u16` `u32` `i8` `i16` `i32` | +| `bigint` | `u64` `u128` `i64` `i128` | +| `number` | `f32` `f64` | +| `number` | `f32` `f64` | +| `boolean` | `bool` | +| `string` | UTF-8 string | +| `type[]` | fixed-size byte array | +| `type[]` | dynamic sized array | +| `object` | enum | +| `Map` | HashMap | +| `Set` | HashSet | +| `null` or `type` | Option | + + +--- ## Contributing @@ -80,4 +119,4 @@ When publishing to npm use [np](https://github.com/sindresorhus/np). This repository is distributed under the terms of both the MIT license and the Apache License (Version 2.0). See [LICENSE-MIT](LICENSE-MIT.txt) and [LICENSE-APACHE](LICENSE-APACHE) for details. -[Borsh]: https://borsh.io +[Borsh]: https://borsh.io \ No newline at end of file diff --git a/borsh-ts/.eslintrc.yml b/borsh-ts/.eslintrc.yml index 82aa9c4de..ea4ae5679 100644 --- a/borsh-ts/.eslintrc.yml +++ b/borsh-ts/.eslintrc.yml @@ -11,7 +11,6 @@ rules: '@typescript-eslint/no-explicit-any': 1 '@typescript-eslint/ban-types': 1 '@typescript-eslint/explicit-function-return-type': 1 - '@typescript-eslint/no-use-before-define': 1 parserOptions: ecmaVersion: 2018 diff --git a/borsh-ts/buffer.ts b/borsh-ts/buffer.ts new file mode 100644 index 000000000..79b0b984d --- /dev/null +++ b/borsh-ts/buffer.ts @@ -0,0 +1,87 @@ +import { IntegerType } from './types.js'; + +export class EncodeBuffer { + offset: number; + buffer_size: number; + buffer: ArrayBuffer; + view: DataView; + + constructor() { + this.offset = 0; + this.buffer_size = 256; + this.buffer = new ArrayBuffer(this.buffer_size); + this.view = new DataView(this.buffer); + } + + resize_if_necessary(needed_space: number): void { + if (this.buffer_size - this.offset < needed_space) { + this.buffer_size = Math.max(this.buffer_size * 2, this.buffer_size + needed_space); + + const new_buffer = new ArrayBuffer(this.buffer_size); + new Uint8Array(new_buffer).set(new Uint8Array(this.buffer)); + + this.buffer = new_buffer; + this.view = new DataView(new_buffer); + } + } + + get_used_buffer(): Uint8Array { + return new Uint8Array(this.buffer).slice(0, this.offset); + } + + store_value(value: number, type: IntegerType): void { + const bSize = type.substring(1); + const size = parseInt(bSize) / 8; + this.resize_if_necessary(size); + + const toCall = type[0] === 'f'? `setFloat${bSize}`: type[0] === 'i'? `setInt${bSize}` : `setUint${bSize}`; + this.view[toCall](this.offset, value, true); + this.offset += size; + } + + store_bytes(from: Uint8Array): void { + this.resize_if_necessary(from.length); + new Uint8Array(this.buffer).set(new Uint8Array(from), this.offset); + this.offset += from.length; + } +} + +export class DecodeBuffer { + offset: number; + buffer_size: number; + buffer: ArrayBuffer; + view: DataView; + + constructor(buf: Uint8Array) { + this.offset = 0; + this.buffer_size = buf.length; + this.buffer = new ArrayBuffer(buf.length); + new Uint8Array(this.buffer).set(buf); + this.view = new DataView(this.buffer); + } + + assert_enough_buffer(size: number): void { + if (this.offset + size > this.buffer.byteLength) { + throw new Error('Error in schema, the buffer is smaller than expected'); + } + } + + consume_value(type: IntegerType): number { + const bSize = type.substring(1); + const size = parseInt(bSize) / 8; + this.assert_enough_buffer(size); + + const toCall = type[0] === 'f'? `getFloat${bSize}`: type[0] === 'i'? `getInt${bSize}` : `getUint${bSize}`; + const ret = this.view[toCall](this.offset, true); + + this.offset += size; + return ret; + } + + consume_bytes(size: number): ArrayBuffer { + this.assert_enough_buffer(size); + const ret = this.buffer.slice(this.offset, this.offset + size); + this.offset += size; + return ret; + } +} \ No newline at end of file diff --git a/borsh-ts/deserialize.ts b/borsh-ts/deserialize.ts new file mode 100644 index 000000000..6a247c683 --- /dev/null +++ b/borsh-ts/deserialize.ts @@ -0,0 +1,125 @@ +import { ArrayType, DecodeTypes, MapType, IntegerType, OptionType, Schema, SetType, StructType, integers, EnumType } from './types.js'; +import { DecodeBuffer } from './buffer.js'; + +export class BorshDeserializer { + buffer: DecodeBuffer; + + constructor(bufferArray: Uint8Array) { + this.buffer = new DecodeBuffer(bufferArray); + } + + decode(schema: Schema): DecodeTypes { + return this.decode_value(schema); + } + + decode_value(schema: Schema): DecodeTypes { + if (typeof schema === 'string') { + if (integers.includes(schema)) return this.decode_integer(schema); + if (schema === 'string') return this.decode_string(); + if (schema === 'bool') return this.decode_boolean(); + } + + if (typeof schema === 'object') { + if ('option' in schema) return this.decode_option(schema as OptionType); + if ('enum' in schema) return this.decode_enum(schema as EnumType); + if ('array' in schema) return this.decode_array(schema as ArrayType); + if ('set' in schema) return this.decode_set(schema as SetType); + if ('map' in schema) return this.decode_map(schema as MapType); + if ('struct' in schema) return this.decode_struct(schema as StructType); + } + + throw new Error(`Unsupported type: ${schema}`); + } + + decode_integer(schema: IntegerType): number | bigint { + const size: number = parseInt(schema.substring(1)); + + if (size <= 32 || schema == 'f64') { + return this.buffer.consume_value(schema); + } + return this.decode_bigint(size, schema.startsWith('i')); + } + + decode_bigint(size: number, signed = false): bigint { + const buffer_len = size / 8; + const buffer = new Uint8Array(this.buffer.consume_bytes(buffer_len)); + const bits = buffer.reduceRight((r, x) => r + x.toString(16).padStart(2, '0'), ''); + + if (signed && buffer[buffer_len - 1]) { + return BigInt.asIntN(size, BigInt(`0x${bits}`)); + } + return BigInt(`0x${bits}`); + } + + decode_string(): string { + const len: number = this.decode_integer('u32') as number; + const buffer = new Uint8Array(this.buffer.consume_bytes(len)); + return String.fromCharCode.apply(null, buffer); + } + + decode_boolean(): boolean { + return this.buffer.consume_value('u8') > 0; + } + + decode_option(schema: OptionType): DecodeTypes { + const option = this.buffer.consume_value('u8'); + if (option === 1) { + return this.decode_value(schema.option); + } + if (option !== 0) { + throw new Error(`Invalid option ${option}`); + } + return null; + } + + decode_enum(schema: EnumType): DecodeTypes { + const valueIndex = this.buffer.consume_value('u8'); + + if (valueIndex > schema.enum.length) { + throw new Error(`Enum option ${valueIndex} is not available`); + } + + const struct = schema.enum[valueIndex].struct; + const key = Object.keys(struct)[0]; + return { [key]: this.decode_value(struct[key]) }; + } + + decode_array(schema: ArrayType): Array { + const result = []; + const len = schema.array.len ? schema.array.len : this.decode_integer('u32') as number; + + for (let i = 0; i < len; ++i) { + result.push(this.decode_value(schema.array.type)); + } + + return result; + } + + decode_set(schema: SetType): Set { + const len = this.decode_integer('u32') as number; + const result = new Set(); + for (let i = 0; i < len; ++i) { + result.add(this.decode_value(schema.set)); + } + return result; + } + + decode_map(schema: MapType): Map { + const len = this.decode_integer('u32') as number; + const result = new Map(); + for (let i = 0; i < len; ++i) { + const key = this.decode_value(schema.map.key); + const value = this.decode_value(schema.map.value); + result.set(key, value); + } + return result; + } + + decode_struct(schema: StructType): object { + const result = {}; + for (const key in schema.struct) { + result[key] = this.decode_value(schema.struct[key]); + } + return result; + } +} \ No newline at end of file diff --git a/borsh-ts/index.ts b/borsh-ts/index.ts index 54e794688..09fe93d46 100644 --- a/borsh-ts/index.ts +++ b/borsh-ts/index.ts @@ -1,477 +1,18 @@ -import BN from 'bn.js'; -import bs58 from 'bs58'; +import { Schema, DecodeTypes } from './types.js'; +import { BorshSerializer } from './serialize.js'; +import { BorshDeserializer } from './deserialize.js'; +import * as utils from './utils.js'; -// TODO: Make sure this polyfill not included when not required -import * as encoding from 'text-encoding-utf-8'; -const ResolvedTextDecoder = - typeof TextDecoder !== 'function' ? encoding.TextDecoder : TextDecoder; -const textDecoder = new ResolvedTextDecoder('utf-8', { fatal: true }); +export { Schema } from './types'; -export function baseEncode(value: Uint8Array | string): string { - if (typeof value === 'string') { - value = Buffer.from(value, 'utf8'); - } - return bs58.encode(Buffer.from(value)); +export function serialize(schema: Schema, value: unknown, validate = true): Uint8Array { + if (validate) utils.validate_schema(schema); + const serializer = new BorshSerializer(validate); + return serializer.encode(value, schema); } -export function baseDecode(value: string): Buffer { - return Buffer.from(bs58.decode(value)); -} - -const INITIAL_LENGTH = 1024; - -export type Schema = Map - -export class BorshError extends Error { - originalMessage: string; - fieldPath: string[] = []; - - constructor(message: string) { - super(message); - this.originalMessage = message; - } - - addToFieldPath(fieldName: string): void { - this.fieldPath.splice(0, 0, fieldName); - // NOTE: Modifying message directly as jest doesn't use .toString() - this.message = this.originalMessage + ': ' + this.fieldPath.join('.'); - } -} - -/// Binary encoder. -export class BinaryWriter { - buf: Buffer; - length: number; - - public constructor() { - this.buf = Buffer.alloc(INITIAL_LENGTH); - this.length = 0; - } - - maybeResize(): void { - if (this.buf.length < 16 + this.length) { - this.buf = Buffer.concat([this.buf, Buffer.alloc(INITIAL_LENGTH)]); - } - } - - public writeU8(value: number): void { - this.maybeResize(); - this.buf.writeUInt8(value, this.length); - this.length += 1; - } - - public writeU16(value: number): void { - this.maybeResize(); - this.buf.writeUInt16LE(value, this.length); - this.length += 2; - } - - public writeU32(value: number): void { - this.maybeResize(); - this.buf.writeUInt32LE(value, this.length); - this.length += 4; - } - - public writeU64(value: number | BN): void { - this.maybeResize(); - this.writeBuffer(Buffer.from(new BN(value).toArray('le', 8))); - } - - public writeU128(value: number | BN): void { - this.maybeResize(); - this.writeBuffer(Buffer.from(new BN(value).toArray('le', 16))); - } - - public writeU256(value: number | BN): void { - this.maybeResize(); - this.writeBuffer(Buffer.from(new BN(value).toArray('le', 32))); - } - - public writeU512(value: number | BN): void { - this.maybeResize(); - this.writeBuffer(Buffer.from(new BN(value).toArray('le', 64))); - } - - private writeBuffer(buffer: Buffer): void { - // Buffer.from is needed as this.buf.subarray can return plain Uint8Array in browser - this.buf = Buffer.concat([ - Buffer.from(this.buf.subarray(0, this.length)), - buffer, - Buffer.alloc(INITIAL_LENGTH), - ]); - this.length += buffer.length; - } - - public writeString(str: string): void { - this.maybeResize(); - const b = Buffer.from(str, 'utf8'); - this.writeU32(b.length); - this.writeBuffer(b); - } - - public writeFixedArray(array: Uint8Array): void { - this.writeBuffer(Buffer.from(array)); - } - - public writeArray(array: any[], fn: any): void { - this.maybeResize(); - this.writeU32(array.length); - for (const elem of array) { - this.maybeResize(); - fn(elem); - } - } - - public toArray(): Uint8Array { - return this.buf.subarray(0, this.length); - } -} - -function handlingRangeError( - target: any, - propertyKey: string, - propertyDescriptor: PropertyDescriptor -): any { - const originalMethod = propertyDescriptor.value; - propertyDescriptor.value = function (...args: any[]): any { - try { - return originalMethod.apply(this, args); - } catch (e) { - if (e instanceof RangeError) { - const code = (e as any).code; - if ( - ['ERR_BUFFER_OUT_OF_BOUNDS', 'ERR_OUT_OF_RANGE'].indexOf(code) >= 0 - ) { - throw new BorshError('Reached the end of buffer when deserializing'); - } - } - throw e; - } - }; -} - -export class BinaryReader { - buf: Buffer; - offset: number; - - public constructor(buf: Buffer) { - this.buf = buf; - this.offset = 0; - } - - @handlingRangeError - readU8(): number { - const value = this.buf.readUInt8(this.offset); - this.offset += 1; - return value; - } - - @handlingRangeError - readU16(): number { - const value = this.buf.readUInt16LE(this.offset); - this.offset += 2; - return value; - } - - @handlingRangeError - readU32(): number { - const value = this.buf.readUInt32LE(this.offset); - this.offset += 4; - return value; - } - - @handlingRangeError - readU64(): BN { - const buf = this.readBuffer(8); - return new BN(buf, 'le'); - } - - @handlingRangeError - readU128(): BN { - const buf = this.readBuffer(16); - return new BN(buf, 'le'); - } - - @handlingRangeError - readU256(): BN { - const buf = this.readBuffer(32); - return new BN(buf, 'le'); - } - - @handlingRangeError - readU512(): BN { - const buf = this.readBuffer(64); - return new BN(buf, 'le'); - } - - private readBuffer(len: number): Buffer { - if (this.offset + len > this.buf.length) { - throw new BorshError(`Expected buffer length ${len} isn't within bounds`); - } - const result = this.buf.slice(this.offset, this.offset + len); - this.offset += len; - return result; - } - - @handlingRangeError - readString(): string { - const len = this.readU32(); - const buf = this.readBuffer(len); - try { - // NOTE: Using TextDecoder to fail on invalid UTF-8 - return textDecoder.decode(buf); - } catch (e) { - throw new BorshError(`Error decoding UTF-8 string: ${e}`); - } - } - - @handlingRangeError - readFixedArray(len: number): Uint8Array { - return new Uint8Array(this.readBuffer(len)); - } - - @handlingRangeError - readArray(fn: any): any[] { - const len = this.readU32(); - const result = Array(); - for (let i = 0; i < len; ++i) { - result.push(fn()); - } - return result; - } -} - -function capitalizeFirstLetter(string): string { - return string.charAt(0).toUpperCase() + string.slice(1); -} - -function serializeField( - schema: Schema, - fieldName: string, - value: any, - fieldType: any, - writer: any -): void { - try { - // TODO: Handle missing values properly (make sure they never result in just skipped write) - if (typeof fieldType === 'string') { - writer[`write${capitalizeFirstLetter(fieldType)}`](value); - } else if (fieldType instanceof Array) { - if (typeof fieldType[0] === 'number') { - if (value.length !== fieldType[0]) { - throw new BorshError( - `Expecting byte array of length ${fieldType[0]}, but got ${value.length} bytes` - ); - } - writer.writeFixedArray(value); - } else if (fieldType.length === 2 && typeof fieldType[1] === 'number') { - if (value.length !== fieldType[1]) { - throw new BorshError( - `Expecting byte array of length ${fieldType[1]}, but got ${value.length} bytes` - ); - } - for (let i = 0; i < fieldType[1]; i++) { - serializeField(schema, null, value[i], fieldType[0], writer); - } - } else { - writer.writeArray(value, (item: any) => { - serializeField(schema, fieldName, item, fieldType[0], writer); - }); - } - } else if (fieldType.kind !== undefined) { - switch (fieldType.kind) { - case 'option': { - if (value === null || value === undefined) { - writer.writeU8(0); - } else { - writer.writeU8(1); - serializeField(schema, fieldName, value, fieldType.type, writer); - } - break; - } - case 'map': { - writer.writeU32(value.size); - value.forEach((val, key) => { - serializeField(schema, fieldName, key, fieldType.key, writer); - serializeField(schema, fieldName, val, fieldType.value, writer); - }); - break; - } - default: - throw new BorshError(`FieldType ${fieldType} unrecognized`); - } - } else { - serializeStruct(schema, value, writer); - } - } catch (error) { - if (error instanceof BorshError) { - error.addToFieldPath(fieldName); - } - throw error; - } -} - -function serializeStruct(schema: Schema, obj: any, writer: BinaryWriter): void { - if (typeof obj.borshSerialize === 'function') { - obj.borshSerialize(writer); - return; - } - - const structSchema = schema.get(obj.constructor); - if (!structSchema) { - throw new BorshError(`Class ${obj.constructor.name} is missing in schema`); - } - - if (structSchema.kind === 'struct') { - structSchema.fields.map(([fieldName, fieldType]: [any, any]) => { - serializeField(schema, fieldName, obj[fieldName], fieldType, writer); - }); - } else if (structSchema.kind === 'enum') { - const name = obj[structSchema.field]; - for (let idx = 0; idx < structSchema.values.length; ++idx) { - const [fieldName, fieldType]: [any, any] = structSchema.values[idx]; - if (fieldName === name) { - writer.writeU8(idx); - serializeField(schema, fieldName, obj[fieldName], fieldType, writer); - break; - } - } - } else { - throw new BorshError( - `Unexpected schema kind: ${structSchema.kind} for ${obj.constructor.name}` - ); - } -} - -/// Serialize given object using schema of the form: -/// { class_name -> [ [field_name, field_type], .. ], .. } -export function serialize( - schema: Schema, - obj: any, - Writer = BinaryWriter -): Uint8Array { - const writer = new Writer(); - serializeStruct(schema, obj, writer); - return writer.toArray(); -} - -function deserializeField( - schema: Schema, - fieldName: string, - fieldType: any, - reader: BinaryReader -): any { - try { - if (typeof fieldType === 'string') { - return reader[`read${capitalizeFirstLetter(fieldType)}`](); - } - - if (fieldType instanceof Array) { - if (typeof fieldType[0] === 'number') { - return reader.readFixedArray(fieldType[0]); - } else if (typeof fieldType[1] === 'number') { - const arr = []; - for (let i = 0; i < fieldType[1]; i++) { - arr.push(deserializeField(schema, null, fieldType[0], reader)); - } - return arr; - } else { - return reader.readArray(() => - deserializeField(schema, fieldName, fieldType[0], reader) - ); - } - } - - if (fieldType.kind === 'option') { - const option = reader.readU8(); - if (option) { - return deserializeField(schema, fieldName, fieldType.type, reader); - } - - return undefined; - } - if (fieldType.kind === 'map') { - const map = new Map(); - const length = reader.readU32(); - for (let i = 0; i < length; i++) { - const key = deserializeField(schema, fieldName, fieldType.key, reader); - const val = deserializeField(schema, fieldName, fieldType.value, reader); - map.set(key, val); - } - return map; - } - - return deserializeStruct(schema, fieldType, reader); - } catch (error) { - if (error instanceof BorshError) { - error.addToFieldPath(fieldName); - } - throw error; - } -} - -function deserializeStruct( - schema: Schema, - classType: any, - reader: BinaryReader -): any { - if (typeof classType.borshDeserialize === 'function') { - return classType.borshDeserialize(reader); - } - - const structSchema = schema.get(classType); - if (!structSchema) { - throw new BorshError(`Class ${classType.name} is missing in schema`); - } - - if (structSchema.kind === 'struct') { - const result = {}; - for (const [fieldName, fieldType] of schema.get(classType).fields) { - result[fieldName] = deserializeField(schema, fieldName, fieldType, reader); - } - return new classType(result); - } - - if (structSchema.kind === 'enum') { - const idx = reader.readU8(); - if (idx >= structSchema.values.length) { - throw new BorshError(`Enum index: ${idx} is out of range`); - } - const [fieldName, fieldType] = structSchema.values[idx]; - const fieldValue = deserializeField(schema, fieldName, fieldType, reader); - return new classType({ [fieldName]: fieldValue }); - } - - throw new BorshError( - `Unexpected schema kind: ${structSchema.kind} for ${classType.constructor.name}` - ); -} - -/// Deserializes object from bytes using schema. -export function deserialize( - schema: Schema, - classType: { new(args: any): T }, - buffer: Buffer, - Reader = BinaryReader -): T { - const reader = new Reader(buffer); - const result = deserializeStruct(schema, classType, reader); - if (reader.offset < buffer.length) { - throw new BorshError( - `Unexpected ${buffer.length - reader.offset - } bytes after deserialized data` - ); - } - return result; -} - -/// Deserializes object from bytes using schema, without checking the length read -export function deserializeUnchecked( - schema: Schema, - classType: { new(args: any): T }, - buffer: Buffer, - Reader = BinaryReader -): T { - const reader = new Reader(buffer); - return deserializeStruct(schema, classType, reader); -} +export function deserialize(schema: Schema, buffer: Uint8Array, validate = true): DecodeTypes { + if (validate) utils.validate_schema(schema); + const deserializer = new BorshDeserializer(buffer); + return deserializer.decode(schema); +} \ No newline at end of file diff --git a/borsh-ts/serialize.ts b/borsh-ts/serialize.ts new file mode 100644 index 000000000..fe7845ac7 --- /dev/null +++ b/borsh-ts/serialize.ts @@ -0,0 +1,177 @@ +import { ArrayType, MapType, IntegerType, OptionType, Schema, SetType, StructType, integers, EnumType } from './types.js'; +import { EncodeBuffer } from './buffer.js'; +import * as utils from './utils.js'; + +export class BorshSerializer { + encoded: EncodeBuffer; + fieldPath: string[]; + checkTypes: boolean; + + constructor(checkTypes) { + this.encoded = new EncodeBuffer(); + this.fieldPath = ['value']; + this.checkTypes = checkTypes; + } + + encode(value: unknown, schema: Schema): Uint8Array { + this.encode_value(value, schema); + return this.encoded.get_used_buffer(); + } + + encode_value(value: unknown, schema: Schema): void { + if (typeof schema === 'string') { + if (integers.includes(schema)) return this.encode_integer(value, schema); + if (schema === 'string') return this.encode_string(value); + if (schema === 'bool') return this.encode_boolean(value); + } + + if (typeof schema === 'object') { + if ('option' in schema) return this.encode_option(value, schema as OptionType); + if ('enum' in schema) return this.encode_enum(value, schema as EnumType); + if ('array' in schema) return this.encode_array(value, schema as ArrayType); + if ('set' in schema) return this.encode_set(value, schema as SetType); + if ('map' in schema) return this.encode_map(value, schema as MapType); + if ('struct' in schema) return this.encode_struct(value, schema as StructType); + } + } + + encode_integer(value: unknown, schema: IntegerType): void { + const size: number = parseInt(schema.substring(1)); + + if (size <= 32 || schema == 'f64') { + this.checkTypes && utils.expect_type(value, 'number', this.fieldPath); + this.encoded.store_value(value as number, schema); + } else { + this.checkTypes && utils.expect_bigint(value, this.fieldPath); + this.encode_bigint(BigInt(value as string), size); + } + } + + encode_bigint(value: bigint, size: number): void { + const buffer_len = size / 8; + const buffer = new Uint8Array(buffer_len); + + for (let i = 0; i < buffer_len; i++) { + buffer[i] = Number(value & BigInt(0xff)); + value = value >> BigInt(8); + } + + this.encoded.store_bytes(new Uint8Array(buffer)); + } + + encode_string(value: unknown): void { + this.checkTypes && utils.expect_type(value, 'string', this.fieldPath); + const _value = value as string; + + // 4 bytes for length + this.encoded.store_value(_value.length, 'u32'); + + // string bytes + for (let i = 0; i < _value.length; i++) { + this.encoded.store_value(_value.charCodeAt(i), 'u8'); + } + } + + encode_boolean(value: unknown): void { + this.checkTypes && utils.expect_type(value, 'boolean', this.fieldPath); + this.encoded.store_value(value as boolean ? 1 : 0, 'u8'); + } + + encode_option(value: unknown, schema: OptionType): void { + if (value === null || value === undefined) { + this.encoded.store_value(0, 'u8'); + } else { + this.encoded.store_value(1, 'u8'); + this.encode_value(value, schema.option); + } + } + + encode_enum(value: unknown, schema: EnumType): void { + this.checkTypes && utils.expect_enum(value, this.fieldPath); + + const valueKey = Object.keys(value)[0]; + + for (let i = 0; i < schema.enum.length; i++) { + const valueSchema = schema.enum[i] as StructType; + + if (valueKey === Object.keys(valueSchema.struct)[0]) { + this.encoded.store_value(i, 'u8'); + return this.encode_struct(value, valueSchema as StructType); + } + } + throw new Error(`Enum key (${valueKey}) not found in enum schema: ${JSON.stringify(schema)} at ${this.fieldPath.join('.')}`); + } + + encode_array(value: unknown, schema: ArrayType): void { + if (utils.isArrayLike(value)) return this.encode_arraylike(value as ArrayLike, schema); + if (value instanceof ArrayBuffer) return this.encode_buffer(value, schema); + throw new Error(`Expected Array-like not ${typeof (value)}(${value}) at ${this.fieldPath.join('.')}`); + } + + encode_arraylike(value: ArrayLike, schema: ArrayType): void { + if (schema.array.len) { + utils.expect_same_size(value.length, schema.array.len, this.fieldPath); + } else { + // 4 bytes for length + this.encoded.store_value(value.length, 'u32'); + } + + // array values + for (let i = 0; i < value.length; i++) { + this.encode_value(value[i], schema.array.type); + } + } + + encode_buffer(value: ArrayBuffer, schema: ArrayType): void { + if (schema.array.len) { + utils.expect_same_size(value.byteLength, schema.array.len, this.fieldPath); + } else { + // 4 bytes for length + this.encoded.store_value(value.byteLength, 'u32'); + } + + // array values + this.encoded.store_bytes(new Uint8Array(value)); + } + + encode_set(value: unknown, schema: SetType): void { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + + const isSet = value instanceof Set; + const values = isSet ? Array.from(value.values()) : Object.values(value); + + // 4 bytes for length + this.encoded.store_value(values.length, 'u32'); + + // set values + for (const value of values) { + this.encode_value(value, schema.set); + } + } + + encode_map(value: unknown, schema: MapType): void { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + + const isMap = value instanceof Map; + const keys = isMap ? Array.from(value.keys()) : Object.keys(value); + + // 4 bytes for length + this.encoded.store_value(keys.length, 'u32'); + + // store key/values + for (const key of keys) { + this.encode_value(key, schema.map.key); + this.encode_value(isMap ? value.get(key) : value[key], schema.map.value); + } + } + + encode_struct(value: unknown, schema: StructType): void { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + + for (const key of Object.keys(schema.struct)) { + this.fieldPath.push(key); + this.encode_value(value[key], schema.struct[key]); + this.fieldPath.pop(); + } + } +} \ No newline at end of file diff --git a/borsh-ts/test/(de)serialize.test.js b/borsh-ts/test/(de)serialize.test.js new file mode 100644 index 000000000..592472609 --- /dev/null +++ b/borsh-ts/test/(de)serialize.test.js @@ -0,0 +1,121 @@ +const borsh = require('../../lib/cjs/index'); +const testStructures = require('./structures'); +const BN = require('bn.js'); + +function check_encode(value, schema, expected) { + const encoded = borsh.serialize(schema, value); + expect(encoded).toEqual(Uint8Array.from(expected)); +} + +function check_decode(expected, schema, encoded) { + const decoded = borsh.deserialize(schema, encoded); + // console.log(decoded, expected); // visual inspection + if (expected instanceof BN) return expect(BigInt(expected) === decoded).toBe(true); + if (schema === 'f32') return expect(decoded).toBeCloseTo(expected); + expect(decoded).toEqual(expected); +} + +function check_roundtrip(value, schema, encoded) { + check_encode(value, schema, encoded); + check_decode(value, schema, encoded); +} + +test('serialize integers', async () => { + check_roundtrip(100, 'u8', [100]); + check_roundtrip(258, 'u16', [2, 1]); + check_roundtrip(102, 'u32', [102, 0, 0, 0]); + check_roundtrip(new BN(103), 'u64', [103, 0, 0, 0, 0, 0, 0, 0]); + check_roundtrip(104n, 'u128', [104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + check_roundtrip(-100, 'i8', [156]); + check_roundtrip(-258, 'i16', [254, 254]); + check_roundtrip(-102, 'i32', [154, 255, 255, 255]); + check_roundtrip(new BN(-103n), 'i64', [153, 255, 255, 255, 255, 255, 255, 255]); + check_roundtrip(-104n, 'i128', [152, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]); +}); + +test('serialize booleans', async () => { + check_roundtrip(true, 'bool', [1]); + check_roundtrip(false, 'bool', [0]); +}); + +test('serialize strings', async () => { + check_roundtrip('h"i', 'string', [3, 0, 0, 0, 104, 34, 105]); +}); + +test('serialize floats', async () => { + check_roundtrip(7.23, 'f64', [236, 81, 184, 30, 133, 235, 28, 64]); + check_roundtrip(7.23, 'f32', [41, 92, 231, 64]); + check_roundtrip(10e2, 'f32', [0, 0, 122, 68]); + check_roundtrip(10e2, 'f64', [0, 0, 0, 0, 0, 64, 143, 64]); +}); + +test('serialize arrays', async () => { + check_roundtrip([true, false], { array: { type: 'bool' } }, [2, 0, 0, 0, 1, 0]); + check_roundtrip([true, false], { array: { type: 'bool', len: 2 } }, [1, 0]); + check_encode(new ArrayBuffer(2), { array: { type: 'u8' } }, [2, 0, 0, 0, 0, 0]); + + const buffer = new ArrayBuffer(2); + new Uint8Array(buffer).set([1, 2]); + check_encode(buffer, { array: { type: 'u8', len: 2 } }, [1, 2]); +}); + +test('serialize options', async () => { + check_roundtrip(null, { option: 'u8' }, [0]); + check_roundtrip(1, { option: 'u32' }, [1, 1, 0, 0, 0]); +}); + +test('serialize maps', async () => { + check_roundtrip(new Map(), { map: { key: 'u8', value: 'u8' } }, [0, 0, 0, 0]); + + const map = new Map(); + map.set('testing', 1); + check_roundtrip(map, { map: { key: 'string', value: 'u32' } }, [1, 0, 0, 0, 7, 0, 0, 0, 116, 101, 115, 116, 105, 110, 103, 1, 0, 0, 0]); + + check_encode({ 'a': 1, 'b': 2 }, { map: { key: 'string', value: 'u8' } }, [2, 0, 0, 0, 1, 0, 0, 0, 97, 1, 1, 0, 0, 0, 98, 2]); +}); + +test('serialize sets', async () => { + check_roundtrip(new Set(), { set: 'u8' }, [0, 0, 0, 0]); + check_roundtrip(new Set([1, 2]), { set: 'u8' }, [2, 0, 0, 0, 1, 2]); +}); + +test('serialize struct', async () => { + check_roundtrip(testStructures.Numbers, testStructures.schemaNumbers, testStructures.encodedNumbers); + check_roundtrip(testStructures.Options, testStructures.schemaOptions, testStructures.encodedOptions); + check_roundtrip(testStructures.Nested, testStructures.schemaNested, testStructures.encodedNested); + check_roundtrip(testStructures.Mixture, testStructures.schemaMixture, testStructures.encodedMixture); + check_roundtrip(testStructures.BigStruct, testStructures.schemaBigStruct, testStructures.encodedBigStruct); +}); + +test('serialize enums', async () => { + const MyEnumNumbers = { numbers: testStructures.Numbers }; + const MyEnumMixture = { mixture: testStructures.Mixture }; + + const enumSchema = { + enum: [{ struct: { numbers: testStructures.schemaNumbers } }, { struct: { mixture: testStructures.schemaMixture } }] + }; + + check_roundtrip(MyEnumNumbers, enumSchema, [0].concat(testStructures.encodedNumbers)); + check_roundtrip(MyEnumMixture, enumSchema, [1].concat(testStructures.encodedMixture)); +}); + +test('(de)serialize follows the schema order', async () => { + const schema = { + struct: { a: 'u8', b: 'u8' } + }; + + const object = { b: 2, a: 1 }; + const encoded = [1, 2]; + + check_encode(object, schema, encoded); + check_decode({ a: 1, b: 2 }, schema, encoded); +}); + +test('errors on invalid values', async () => { + const schema_array = { array: { type: 'u16' } }; + + expect(() => check_encode(['a'], schema_array, [])).toThrow('Expected number not string(a) at value'); + expect(() => check_encode(3, 'string', [])).toThrow('Expected string not number(3) at value'); + expect(() => check_encode({ 'a': 1, 'b': '2' }, { struct: { a: 'u8', b: 'u8' } }, [])).toThrow('Expected number not string(2) at value.b'); + expect(() => check_encode({ 'a': { 'b': { 'c': 3 } } }, { struct: { a: { struct: { b: { struct: { c: 'string' } } } } } }, [])).toThrow('Expected string not number(3) at value.a.b.c'); +}); \ No newline at end of file diff --git a/borsh-ts/test/.eslintrc.yml b/borsh-ts/test/.eslintrc.yml index ab46369c7..979617f72 100644 --- a/borsh-ts/test/.eslintrc.yml +++ b/borsh-ts/test/.eslintrc.yml @@ -1,3 +1,10 @@ extends: '../../.eslintrc.yml' env: + es6: true jest: true + es2020: true +rules: + no-inner-declarations: 1 + '@typescript-eslint/no-explicit-any': 1 + '@typescript-eslint/explicit-function-return-type': 0 + '@typescript-eslint/no-var-requires': 0 diff --git a/borsh-ts/test/fuzz/borsh-roundtrip.js b/borsh-ts/test/fuzz/borsh-roundtrip.js deleted file mode 100644 index f790375d4..000000000 --- a/borsh-ts/test/fuzz/borsh-roundtrip.js +++ /dev/null @@ -1,19 +0,0 @@ -const borsh = require('../../../lib/index.js'); -const transaction = require('./transaction-example/transaction'); - -exports.fuzz = input => { - try { - const deserialized = borsh.deserialize(transaction.SCHEMA, transaction.Transaction, input); - const serialized = borsh.serialize(transaction.SCHEMA, deserialized); - if (!serialized.equals(input)) { - console.log(`Mismatching output:\n${serialized.toString('hex')}\nand input:\n${input.toString('hex')}`); - throw new Error('Mismatching input and output'); - } - } catch (e) { - if (e instanceof borsh.BorshError) { - // Do nothing - } else { - throw e; - } - } -}; \ No newline at end of file diff --git a/borsh-ts/test/fuzz/corpus/004b705c22403d1c22ceab37181f6340b5b269b7faee05d97fe65b1ab91cd171 b/borsh-ts/test/fuzz/corpus/004b705c22403d1c22ceab37181f6340b5b269b7faee05d97fe65b1ab91cd171 deleted file mode 100644 index fcfea9a29697dc2d94ce838889c3553ef75f0a19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmZQz0D{Vx?OVSVnLc;ASUO?Sher&B3(jhP=9ABmZQx6v94FAh2vP(DVnAG6Qks&P zuUl50?3-F#oLFj^YGRn{`@Xrlxz%|lzj4F*J}nFJyWja=PGdCPwDwX|`fM%6w;%(U ixPU}rN(xw8dMY=NSFKc%T2Z28rKD8b&>{yEzytt67B2As diff --git a/borsh-ts/test/fuzz/corpus/25b2da453a8cb0d574d31a06f0a57fc7b5f2cb5c08ba00ff900a79ebd0637da7 b/borsh-ts/test/fuzz/corpus/25b2da453a8cb0d574d31a06f0a57fc7b5f2cb5c08ba00ff900a79ebd0637da7 deleted file mode 100644 index 3e55b9a7f45b59ea242c6cdbd0dee8f1d4104891..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn$iPsoRFYaz0#XbF z0zh0`Qks&PuUl50TxywWVwmguzPY-&)p;hral`sPEer9x-}zroV>I2g_EJ>(Y%RvO lKrMGh!{2>`26FGm0X diff --git a/borsh-ts/test/fuzz/corpus/2f6f8117189cb82e111a13b3ffb19fcaf35deef41f8c084726c5aac5a4551696 b/borsh-ts/test/fuzz/corpus/2f6f8117189cb82e111a13b3ffb19fcaf35deef41f8c084726c5aac5a4551696 deleted file mode 100644 index 9f09c84d6a89cc14a382a943328c8fd347767bd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn$iPsoRFYaz0#y3| zhy{SSxTG{CGher?Jh{{|)xgHDGnf%5L>-)4U#P5FRe>siObko}Z4C%AA7~e9o{{PR) s1=O3ElH!|MT%4Gm$_?aIE0v^Hlqgv#Db+T#$N>dFfbkat0}w$00NhV8CjbBd diff --git a/borsh-ts/test/fuzz/corpus/4eb3f263ac0d9bac83d67e28d3381c64c61654579dc455a9e5ccaa54bc093721 b/borsh-ts/test/fuzz/corpus/4eb3f263ac0d9bac83d67e28d3381c64c61654579dc455a9e5ccaa54bc093721 deleted file mode 100644 index 0ea79f1823ab375505fa9bf959a93ba6590bb23d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn2vWr0#vlMBi%UvV zGV^uI%9BeiQ*(XaH&>e&f~n?K=b8M*4eR@~OvLYg=YKhk(QMP&OHt{wwHV)m3}E5{ k5{W4(zNy8i3><1 nrlk0$78fU`!*!-|1BI)VN>VFIl&qALY8zVQfC5M$62b-mG&nH7 diff --git a/borsh-ts/test/fuzz/corpus/67fc403d488908e0a27d74a2de056c34c40850bbe61b9ebc82cf48365484f4fc b/borsh-ts/test/fuzz/corpus/67fc403d488908e0a27d74a2de056c34c40850bbe61b9ebc82cf48365484f4fc deleted file mode 100644 index 77435fee1f33b572c943a5939a934f1d0d8d127a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn2vP(D0zh0`Qks&P zuUl50TxywWVwmguzPY-&)p;hral`sPEer9x-}zroV>I2g_EJ>(Y%RvOAiYdnKq4_E g#W%IMI59ny8_26xDoL#bI0}5aQ01<&Ki3>=; cc(^b diff --git a/borsh-ts/test/fuzz/corpus/824d229c23c200a937bcd558a5c55d25830a8efffb9e8661a81e2db3584ca512 b/borsh-ts/test/fuzz/corpus/824d229c23c200a937bcd558a5c55d25830a8efffb9e8661a81e2db3584ca512 deleted file mode 100644 index f7c9ac58b4c75f59f733b9ea2d09e7f718b858af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn2vWr0#vlMBi%UvV zGV^uI%9BeiQ%wwWecv}%H@7;^i3><1 irlk0$78fU`r*Z>%)k-C)6(vemN=mg2Epk8sOaK5dAuTHa diff --git a/borsh-ts/test/fuzz/corpus/9d6a787b7d95c13bb67d02137cf3e506f550fcfa19363050d1e4917250d83d32 b/borsh-ts/test/fuzz/corpus/9d6a787b7d95c13bb67d02137cf3e506f550fcfa19363050d1e4917250d83d32 deleted file mode 100644 index 630b94ad0b64803456fe83b17806ed1c307dc17f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn$iPsoRFYaz0#XbF z0zh0`Qks&PuUl50TxywWVwjutzPY-&)p;hral`sPEer9x-}zroV>I2g_EJ>(Y%RvO oKre&f~n?K=b8M*4eR@~EX41A=YKhk(QMP&OHt{wwHV)m3}E5{ k5{W4(zNy8ecv}%H@7;^7e+ZXmB(sU)?cM9E4?skWg-4k!QyzZe*R2nqm>U@)}+ diff --git a/borsh-ts/test/fuzz/corpus/b7361e727623b1cdafda1411e27c119009d0fcccbdddd5fe669c8c02bf04f7dd b/borsh-ts/test/fuzz/corpus/b7361e727623b1cdafda1411e27c119009d0fcccbdddd5fe669c8c02bf04f7dd deleted file mode 100644 index d0f9ae0329c0ca85e30e55962b0d41ade18cc9cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 170 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn2vWr0#vlMBi%UvV zGF|F#&3%48U$?A0xzsY%#4y+QeRFkltMg2L2{g&QxxoaJ5oNYDI~Xm6B3zLyH_x00~4w*Z^FmG_L>v diff --git a/borsh-ts/test/fuzz/corpus/b98f80e8a618038000215158136c1f2dbd9ed3cad450556faf7b18db4184b42e b/borsh-ts/test/fuzz/corpus/b98f80e8a618038000215158136c1f2dbd9ed3cad450556faf7b18db4184b42e deleted file mode 100644 index 0181e35d13a2d5fe0a567dd7e7fa99296ad0245a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn2vWqr$RGeDi%UvV zGV^uI%9BeiQ%wwWecv}%H@7;^i3><1 irlk0$78fU`r*Z>%)k-C)6(vemN=mg2Epk8sOaK4|-YmHQ diff --git a/borsh-ts/test/fuzz/corpus/bec080831059d96afd82176d6e8ee36819bb22b7304f4e23f61d189a6c221f92 b/borsh-ts/test/fuzz/corpus/bec080831059d96afd82176d6e8ee36819bb22b7304f4e23f61d189a6c221f92 deleted file mode 100644 index b04b3f05854aa3d7637fae48b14de189d4a50cb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164 zcmZQz0D{Vx?OVSVnLc+)ES)gv!y|^m1!uKC^T}t(Ht?lSjuYr$1StXnF(583DNV`D z*DWhg_DwA=zF2CRYGRn{`@Xrlxz%|lzj4F*KCPG27)v*;y%d!`TZZv1NG}r?kVs5P o0qICgFHeSQO63L$S1XmIR+K1NDJj)9w8#MkV3q-ykw6Rt0QN{QR{#J2 diff --git a/borsh-ts/test/fuzz/corpus/cf94b13d93b7accf950d3d2a1be308f7d25d21f81622a7ffc7da3733c03eb572 b/borsh-ts/test/fuzz/corpus/cf94b13d93b7accf950d3d2a1be308f7d25d21f81622a7ffc7da3733c03eb572 deleted file mode 100644 index 5ade500ec2b0052071865b84132da79f2fe64e4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmZQz0D{Vx?OVSVnLc;ASUO?Sher&B3(jhP=9ABmZQx6v94FAh$WRMX#vlg7#U-UF znfbb9<;ibTi;ELWEmKVlbA8`8S2wph&*V35Sl_2*A%6Ee|I2BNrkmDYib|iY#pufj m)X2mIBob3nd_mgMQ@Me>YNe9YiV`I&B_)Q27CE2*CIA46HZJi1 diff --git a/borsh-ts/test/fuzz/corpus/d2897d4dc13f861fb5e7caf8e5b7e1c9962a36c8489ada9cb0f798c1ec967eb7 b/borsh-ts/test/fuzz/corpus/d2897d4dc13f861fb5e7caf8e5b7e1c9962a36c8489ada9cb0f798c1ec967eb7 deleted file mode 100644 index 86c116b9414d856cb4b88364dfc37699623ba442..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn2vP(D0zh0`Qks&P wuUl50TxywWVwmguzPY-&)p;hral`sPEer9x-}zroV>I2g_EJi3><1 lrlk0$78fU`r*Z>%)k-C)6(vemN=mg2Epk8sG>~dy2mt&aEnolu diff --git a/borsh-ts/test/fuzz/corpus/e08ccfae0c2fd6432c8e1bf841050f0b2d78fa519ac76f178db0757d1bd3e96e b/borsh-ts/test/fuzz/corpus/e08ccfae0c2fd6432c8e1bf841050f0b2d78fa519ac76f178db0757d1bd3e96e deleted file mode 100644 index 212ab60fe57278568a39376e388dee96da8129a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmZQz0D{Vx?OVSVDLr?(SUO?Sher&B3(jhP=9ABmZQx6v94FSn2vWr0#vlMBi%UvV zGV^uI%9BeiznB>2`o3?jZf2kA@&=}hGY3Rf$Yq*jzDSt%*iHnhkA1z-T-7NAHZL=*tFkT+%k diff --git a/borsh-ts/test/fuzz/corpus/ecd71b60d1820dbbcb06d1f981ab86d16876888bb7ba665637c5717c0a8916b2 b/borsh-ts/test/fuzz/corpus/ecd71b60d1820dbbcb06d1f981ab86d16876888bb7ba665637c5717c0a8916b2 deleted file mode 100644 index 82dc8f8e8159af961fe9392b5612129507348459..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmZQz0D{Vx?OVSVDLr?(SUO>nz$1pj1!uKC^T}t(Ht?lSjuY!(1Sw*0V-Nt6#U-UF znfbb9<;kU%A508$ecv}%H@7;^i3><1 irlk0$78fU`r*Z>%)k-C)6(vemN=mg2Epk8sOaK7uJS^4# diff --git a/borsh-ts/test/fuzz/transaction-example/enums.d.ts b/borsh-ts/test/fuzz/transaction-example/enums.d.ts deleted file mode 100644 index 4d0a3dae9..000000000 --- a/borsh-ts/test/fuzz/transaction-example/enums.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export declare abstract class Enum { - enum: string; - constructor(properties: any); -} -export declare abstract class Assignable { - constructor(properties: any); -} diff --git a/borsh-ts/test/fuzz/transaction-example/enums.js b/borsh-ts/test/fuzz/transaction-example/enums.js deleted file mode 100644 index 2351f87ff..000000000 --- a/borsh-ts/test/fuzz/transaction-example/enums.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); -class Enum { - constructor(properties) { - if (Object.keys(properties).length !== 1) { - throw new Error('Enum can only take single value'); - } - Object.keys(properties).map((key) => { - this[key] = properties[key]; - this.enum = key; - }); - } -} -exports.Enum = Enum; -class Assignable { - constructor(properties) { - Object.keys(properties).map((key) => { - this[key] = properties[key]; - }); - } -} -exports.Assignable = Assignable; diff --git a/borsh-ts/test/fuzz/transaction-example/key_pair.d.ts b/borsh-ts/test/fuzz/transaction-example/key_pair.d.ts deleted file mode 100644 index cf7a473d2..000000000 --- a/borsh-ts/test/fuzz/transaction-example/key_pair.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Assignable } from './enums'; -export declare type Arrayish = string | ArrayLike; -export interface Signature { - signature: Uint8Array; - publicKey: PublicKey; -} -/** All supported key types */ -export declare enum KeyType { - ED25519 = 0 -} -/** - * PublicKey representation that has type and bytes of the key. - */ -export declare class PublicKey extends Assignable { - keyType: KeyType; - data: Uint8Array; - static from(value: string | PublicKey): PublicKey; - static fromString(encodedKey: string): PublicKey; - toString(): string; -} -export declare abstract class KeyPair { - abstract sign(message: Uint8Array): Signature; - abstract verify(message: Uint8Array, signature: Uint8Array): boolean; - abstract toString(): string; - abstract getPublicKey(): PublicKey; - /** - * @param curve Name of elliptical curve, case-insensitive - * @returns Random KeyPair based on the curve - */ - static fromRandom(curve: string): KeyPair; - static fromString(encodedKey: string): KeyPair; -} -/** - * This class provides key pair functionality for Ed25519 curve: - * generating key pairs, encoding key pairs, signing and verifying. - */ -export declare class KeyPairEd25519 extends KeyPair { - readonly publicKey: PublicKey; - readonly secretKey: string; - /** - * Construct an instance of key pair given a secret key. - * It's generally assumed that these are encoded in base58. - * @param {string} secretKey - */ - constructor(secretKey: string); - /** - * Generate a new random keypair. - * @example - * const keyRandom = KeyPair.fromRandom(); - * keyRandom.publicKey - * // returns [PUBLIC_KEY] - * - * keyRandom.secretKey - * // returns [SECRET_KEY] - */ - static fromRandom(): KeyPairEd25519; - sign(message: Uint8Array): Signature; - verify(message: Uint8Array, signature: Uint8Array): boolean; - toString(): string; - getPublicKey(): PublicKey; -} diff --git a/borsh-ts/test/fuzz/transaction-example/key_pair.js b/borsh-ts/test/fuzz/transaction-example/key_pair.js deleted file mode 100644 index c49f3890d..000000000 --- a/borsh-ts/test/fuzz/transaction-example/key_pair.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { 'default': mod }; -}; -Object.defineProperty(exports, '__esModule', { value: true }); -const tweetnacl_1 = __importDefault(require('tweetnacl')); -const serialize_1 = require('./serialize'); -const enums_1 = require('./enums'); -/** All supported key types */ -var KeyType; -(function (KeyType) { - KeyType[KeyType['ED25519'] = 0] = 'ED25519'; -})(KeyType = exports.KeyType || (exports.KeyType = {})); -function key_type_to_str(keyType) { - switch (keyType) { - case KeyType.ED25519: return 'ed25519'; - default: throw new Error(`Unknown key type ${keyType}`); - } -} -function str_to_key_type(keyType) { - switch (keyType.toLowerCase()) { - case 'ed25519': return KeyType.ED25519; - default: throw new Error(`Unknown key type ${keyType}`); - } -} -/** - * PublicKey representation that has type and bytes of the key. - */ -class PublicKey extends enums_1.Assignable { - static from(value) { - if (typeof value === 'string') { - return PublicKey.fromString(value); - } - return value; - } - static fromString(encodedKey) { - const parts = encodedKey.split(':'); - if (parts.length === 1) { - return new PublicKey({ keyType: KeyType.ED25519, data: serialize_1.base_decode(parts[0]) }); - } - else if (parts.length === 2) { - return new PublicKey({ keyType: str_to_key_type(parts[0]), data: serialize_1.base_decode(parts[1]) }); - } - else { - throw new Error('Invalid encoded key format, must be :'); - } - } - toString() { - return `${key_type_to_str(this.keyType)}:${serialize_1.base_encode(this.data)}`; - } -} -exports.PublicKey = PublicKey; -class KeyPair { - /** - * @param curve Name of elliptical curve, case-insensitive - * @returns Random KeyPair based on the curve - */ - static fromRandom(curve) { - switch (curve.toUpperCase()) { - case 'ED25519': return KeyPairEd25519.fromRandom(); - default: throw new Error(`Unknown curve ${curve}`); - } - } - static fromString(encodedKey) { - const parts = encodedKey.split(':'); - if (parts.length === 1) { - return new KeyPairEd25519(parts[0]); - } - else if (parts.length === 2) { - switch (parts[0].toUpperCase()) { - case 'ED25519': return new KeyPairEd25519(parts[1]); - default: throw new Error(`Unknown curve: ${parts[0]}`); - } - } - else { - throw new Error('Invalid encoded key format, must be :'); - } - } -} -exports.KeyPair = KeyPair; -/** - * This class provides key pair functionality for Ed25519 curve: - * generating key pairs, encoding key pairs, signing and verifying. - */ -class KeyPairEd25519 extends KeyPair { - /** - * Construct an instance of key pair given a secret key. - * It's generally assumed that these are encoded in base58. - * @param {string} secretKey - */ - constructor(secretKey) { - super(); - const keyPair = tweetnacl_1.default.sign.keyPair.fromSecretKey(serialize_1.base_decode(secretKey)); - this.publicKey = new PublicKey({ keyType: KeyType.ED25519, data: keyPair.publicKey }); - this.secretKey = secretKey; - } - /** - * Generate a new random keypair. - * @example - * const keyRandom = KeyPair.fromRandom(); - * keyRandom.publicKey - * // returns [PUBLIC_KEY] - * - * keyRandom.secretKey - * // returns [SECRET_KEY] - */ - static fromRandom() { - const newKeyPair = tweetnacl_1.default.sign.keyPair(); - return new KeyPairEd25519(serialize_1.base_encode(newKeyPair.secretKey)); - } - sign(message) { - const signature = tweetnacl_1.default.sign.detached(message, serialize_1.base_decode(this.secretKey)); - return { signature, publicKey: this.publicKey }; - } - verify(message, signature) { - return tweetnacl_1.default.sign.detached.verify(message, signature, this.publicKey.data); - } - toString() { - return `ed25519:${this.secretKey}`; - } - getPublicKey() { - return this.publicKey; - } -} -exports.KeyPairEd25519 = KeyPairEd25519; diff --git a/borsh-ts/test/fuzz/transaction-example/serialize.d.ts b/borsh-ts/test/fuzz/transaction-example/serialize.d.ts deleted file mode 100644 index 6db96b581..000000000 --- a/borsh-ts/test/fuzz/transaction-example/serialize.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -/// -import BN from 'bn.js'; -export declare function base_encode(value: Uint8Array | string): string; -export declare function base_decode(value: string): Uint8Array; -export declare type Schema = Map; -export declare class BorshError extends Error { - originalMessage: string; - fieldPath: string[]; - constructor(message: string); - addToFieldPath(fieldName: string): void; -} -export declare class BinaryWriter { - buf: Buffer; - length: number; - constructor(); - maybe_resize(): void; - write_u8(value: number): void; - write_u32(value: number): void; - write_u64(value: BN): void; - write_u128(value: BN): void; - private write_buffer; - write_string(str: string): void; - write_fixed_array(array: Uint8Array): void; - write_array(array: any[], fn: any): void; - toArray(): Uint8Array; -} -export declare class BinaryReader { - buf: Buffer; - offset: number; - constructor(buf: Buffer); - read_u8(): number; - read_u32(): number; - read_u64(): BN; - read_u128(): BN; - private read_buffer; - read_string(): string; - read_fixed_array(len: number): Uint8Array; - read_array(fn: any): any[]; -} -export declare function serialize(schema: Schema, obj: any): Uint8Array; -export declare function deserialize(schema: Schema, classType: any, buffer: Buffer): any; diff --git a/borsh-ts/test/fuzz/transaction-example/serialize.js b/borsh-ts/test/fuzz/transaction-example/serialize.js deleted file mode 100644 index 2437c3072..000000000 --- a/borsh-ts/test/fuzz/transaction-example/serialize.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { 'default': mod }; -}; -const bs58_1 = __importDefault(require('bs58')); -function base_encode(value) { - if (typeof (value) === 'string') { - value = Buffer.from(value, 'utf8'); - } - return bs58_1.default.encode(Buffer.from(value)); -} -exports.base_encode = base_encode; -function base_decode(value) { - return Buffer.from(bs58_1.default.decode(value)); -} -exports.base_decode = base_decode; diff --git a/borsh-ts/test/fuzz/transaction-example/signer.d.ts b/borsh-ts/test/fuzz/transaction-example/signer.d.ts deleted file mode 100644 index 36775b637..000000000 --- a/borsh-ts/test/fuzz/transaction-example/signer.d.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Signature, KeyPair, PublicKey } from './key_pair'; -import { KeyStore } from './key_stores'; -/** - * General signing interface, can be used for in memory signing, RPC singing, external wallet, HSM, etc. - */ -export declare abstract class Signer { - /** - * Creates new key and returns public key. - */ - abstract createKey(accountId: string, networkId?: string): Promise; - /** - * Returns public key for given account / network. - * @param accountId accountId to retrieve from. - * @param networkId The targeted network. (ex. default, betanet, etc…) - */ - abstract getPublicKey(accountId?: string, networkId?: string): Promise; - /** - * Signs given message, by first hashing with sha256. - * @param message message to sign. - * @param accountId accountId to use for signing. - * @param networkId The targeted network. (ex. default, betanet, etc…) - */ - abstract signMessage(message: Uint8Array, accountId?: string, networkId?: string): Promise; -} -/** - * Signs using in memory key store. - */ -export declare class InMemorySigner extends Signer { - readonly keyStore: KeyStore; - constructor(keyStore: KeyStore); - /** - * Creates a single account Signer instance with account, network and keyPair provided. - * - * Intended to be useful for temporary keys (e.g. claiming a Linkdrop). - * - * @param networkId The targeted network. (ex. default, betanet, etc…) - * @param accountId The NEAR account to assign the key pair to - * @param keyPair The keyPair to use for signing - */ - static fromKeyPair(networkId: string, accountId: string, keyPair: KeyPair): Promise; - /** - * Creates a public key for the account given - * @param accountId The NEAR account to assign a public key to - * @param networkId The targeted network. (ex. default, betanet, etc…) - * @returns {Promise} - */ - createKey(accountId: string, networkId: string): Promise; - /** - * Gets the existing public key for a given account - * @param accountId The NEAR account to assign a public key to - * @param networkId The targeted network. (ex. default, betanet, etc…) - * @returns {Promise} Returns the public key or null if not found - */ - getPublicKey(accountId?: string, networkId?: string): Promise; - /** - * @param message A message to be signed, typically a serialized transaction - * @param accountId the NEAR account signing the message - * @param networkId The targeted network. (ex. default, betanet, etc…) - * @returns {Promise} - */ - signMessage(message: Uint8Array, accountId?: string, networkId?: string): Promise; - toString(): string; -} diff --git a/borsh-ts/test/fuzz/transaction-example/signer.js b/borsh-ts/test/fuzz/transaction-example/signer.js deleted file mode 100644 index 00eec2654..000000000 --- a/borsh-ts/test/fuzz/transaction-example/signer.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { 'default': mod }; -}; -Object.defineProperty(exports, '__esModule', { value: true }); -const js_sha256_1 = __importDefault(require('js-sha256')); -const key_pair_1 = require('./key_pair'); -const key_stores_1 = require('./key_stores'); -/** - * General signing interface, can be used for in memory signing, RPC singing, external wallet, HSM, etc. - */ -class Signer { -} -exports.Signer = Signer; -/** - * Signs using in memory key store. - */ -class InMemorySigner extends Signer { - constructor(keyStore) { - super(); - this.keyStore = keyStore; - } - /** - * Creates a single account Signer instance with account, network and keyPair provided. - * - * Intended to be useful for temporary keys (e.g. claiming a Linkdrop). - * - * @param networkId The targeted network. (ex. default, betanet, etc…) - * @param accountId The NEAR account to assign the key pair to - * @param keyPair The keyPair to use for signing - */ - static async fromKeyPair(networkId, accountId, keyPair) { - const keyStore = new key_stores_1.InMemoryKeyStore(); - await keyStore.setKey(networkId, accountId, keyPair); - return new InMemorySigner(keyStore); - } - /** - * Creates a public key for the account given - * @param accountId The NEAR account to assign a public key to - * @param networkId The targeted network. (ex. default, betanet, etc…) - * @returns {Promise} - */ - async createKey(accountId, networkId) { - const keyPair = key_pair_1.KeyPair.fromRandom('ed25519'); - await this.keyStore.setKey(networkId, accountId, keyPair); - return keyPair.getPublicKey(); - } - /** - * Gets the existing public key for a given account - * @param accountId The NEAR account to assign a public key to - * @param networkId The targeted network. (ex. default, betanet, etc…) - * @returns {Promise} Returns the public key or null if not found - */ - async getPublicKey(accountId, networkId) { - const keyPair = await this.keyStore.getKey(networkId, accountId); - if (keyPair === null) { - return null; - } - return keyPair.getPublicKey(); - } - /** - * @param message A message to be signed, typically a serialized transaction - * @param accountId the NEAR account signing the message - * @param networkId The targeted network. (ex. default, betanet, etc…) - * @returns {Promise} - */ - async signMessage(message, accountId, networkId) { - const hash = new Uint8Array(js_sha256_1.default.sha256.array(message)); - if (!accountId) { - throw new Error('InMemorySigner requires provided account id'); - } - const keyPair = await this.keyStore.getKey(networkId, accountId); - if (keyPair === null) { - throw new Error(`Key for ${accountId} not found in ${networkId}`); - } - return keyPair.sign(hash); - } - toString() { - return `InMemorySigner(${this.keyStore})`; - } -} -exports.InMemorySigner = InMemorySigner; diff --git a/borsh-ts/test/fuzz/transaction-example/transaction.d.ts b/borsh-ts/test/fuzz/transaction-example/transaction.d.ts deleted file mode 100644 index bedf27f31..000000000 --- a/borsh-ts/test/fuzz/transaction-example/transaction.d.ts +++ /dev/null @@ -1,106 +0,0 @@ -/// -import BN from 'bn.js'; -import { Enum, Assignable } from './enums'; -import { KeyType, PublicKey } from './key_pair'; -import { Signer } from './signer'; -export declare class FunctionCallPermission extends Assignable { - allowance?: BN; - receiverId: string; - methodNames: string[]; -} -export declare class FullAccessPermission extends Assignable { -} -export declare class AccessKeyPermission extends Enum { - functionCall: FunctionCallPermission; - fullAccess: FullAccessPermission; -} -export declare class AccessKey extends Assignable { - nonce: number; - permission: AccessKeyPermission; -} -export declare function fullAccessKey(): AccessKey; -export declare function functionCallAccessKey(receiverId: string, methodNames: string[], allowance?: BN): AccessKey; -export declare class IAction extends Assignable { -} -export declare class CreateAccount extends IAction { -} -export declare class DeployContract extends IAction { - code: Uint8Array; -} -export declare class FunctionCall extends IAction { - methodName: string; - args: Uint8Array; - gas: BN; - deposit: BN; -} -export declare class Transfer extends IAction { - deposit: BN; -} -export declare class Stake extends IAction { - stake: BN; - publicKey: PublicKey; -} -export declare class AddKey extends IAction { - publicKey: PublicKey; - accessKey: AccessKey; -} -export declare class DeleteKey extends IAction { - publicKey: PublicKey; -} -export declare class DeleteAccount extends IAction { - beneficiaryId: string; -} -export declare function createAccount(): Action; -export declare function deployContract(code: Uint8Array): Action; -/** - * Constructs {@link Action} instance representing contract method call. - * - * @param methodName the name of the method to call - * @param args arguments to pass to method. Can be either plain JS object which gets serialized as JSON automatically - * or `Uint8Array` instance which represents bytes passed as is. - * @param gas max amount of gas that method call can use - * @param deposit amount of NEAR (in yoctoNEAR) to send together with the call - */ -export declare function functionCall(methodName: string, args: Uint8Array | object, gas: BN, deposit: BN): Action; -export declare function transfer(deposit: BN): Action; -export declare function stake(stake: BN, publicKey: PublicKey): Action; -export declare function addKey(publicKey: PublicKey, accessKey: AccessKey): Action; -export declare function deleteKey(publicKey: PublicKey): Action; -export declare function deleteAccount(beneficiaryId: string): Action; -export declare class Signature extends Assignable { - keyType: KeyType; - data: Uint8Array; -} -export declare class Transaction extends Assignable { - signerId: string; - publicKey: PublicKey; - nonce: number; - receiverId: string; - actions: Action[]; - blockHash: Uint8Array; - encode(): Uint8Array; - static decode(bytes: Buffer): Transaction; -} -export declare class SignedTransaction extends Assignable { - transaction: Transaction; - signature: Signature; - encode(): Uint8Array; - static decode(bytes: Buffer): SignedTransaction; -} -/** - * Contains a list of the valid transaction Actions available with this API - */ -export declare class Action extends Enum { - createAccount: CreateAccount; - deployContract: DeployContract; - functionCall: FunctionCall; - transfer: Transfer; - stake: Stake; - addKey: AddKey; - deleteKey: DeleteKey; - deleteAccount: DeleteAccount; -} -export declare const SCHEMA: Map; -export declare function createTransaction(signerId: string, publicKey: PublicKey, receiverId: string, nonce: number, actions: Action[], blockHash: Uint8Array): Transaction; -export declare function signTransaction(transaction: Transaction, signer: Signer, accountId?: string, networkId?: string): Promise<[Uint8Array, SignedTransaction]>; -export declare function signTransaction(receiverId: string, nonce: number, actions: Action[], blockHash: Uint8Array, signer: Signer, accountId?: string, networkId?: string): Promise<[Uint8Array, SignedTransaction]>; diff --git a/borsh-ts/test/fuzz/transaction-example/transaction.js b/borsh-ts/test/fuzz/transaction-example/transaction.js deleted file mode 100644 index 50aa7c8bb..000000000 --- a/borsh-ts/test/fuzz/transaction-example/transaction.js +++ /dev/null @@ -1,234 +0,0 @@ -'use strict'; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { 'default': mod }; -}; -Object.defineProperty(exports, '__esModule', { value: true }); -const js_sha256_1 = __importDefault(require('js-sha256')); -const enums_1 = require('./enums'); -const serialize_1 = require('./serialize'); -const key_pair_1 = require('./key_pair'); -class FunctionCallPermission extends enums_1.Assignable { -} -exports.FunctionCallPermission = FunctionCallPermission; -class FullAccessPermission extends enums_1.Assignable { -} -exports.FullAccessPermission = FullAccessPermission; -class AccessKeyPermission extends enums_1.Enum { -} -exports.AccessKeyPermission = AccessKeyPermission; -class AccessKey extends enums_1.Assignable { -} -exports.AccessKey = AccessKey; -function fullAccessKey() { - return new AccessKey({ nonce: 0, permission: new AccessKeyPermission({ fullAccess: new FullAccessPermission({}) }) }); -} -exports.fullAccessKey = fullAccessKey; -function functionCallAccessKey(receiverId, methodNames, allowance) { - return new AccessKey({ nonce: 0, permission: new AccessKeyPermission({ functionCall: new FunctionCallPermission({ receiverId, allowance, methodNames }) }) }); -} -exports.functionCallAccessKey = functionCallAccessKey; -class IAction extends enums_1.Assignable { -} -exports.IAction = IAction; -class CreateAccount extends IAction { -} -exports.CreateAccount = CreateAccount; -class DeployContract extends IAction { -} -exports.DeployContract = DeployContract; -class FunctionCall extends IAction { -} -exports.FunctionCall = FunctionCall; -class Transfer extends IAction { -} -exports.Transfer = Transfer; -class Stake extends IAction { -} -exports.Stake = Stake; -class AddKey extends IAction { -} -exports.AddKey = AddKey; -class DeleteKey extends IAction { -} -exports.DeleteKey = DeleteKey; -class DeleteAccount extends IAction { -} -exports.DeleteAccount = DeleteAccount; -function createAccount() { - return new Action({ createAccount: new CreateAccount({}) }); -} -exports.createAccount = createAccount; -function deployContract(code) { - return new Action({ deployContract: new DeployContract({ code }) }); -} -exports.deployContract = deployContract; -/** - * Constructs {@link Action} instance representing contract method call. - * - * @param methodName the name of the method to call - * @param args arguments to pass to method. Can be either plain JS object which gets serialized as JSON automatically - * or `Uint8Array` instance which represents bytes passed as is. - * @param gas max amount of gas that method call can use - * @param deposit amount of NEAR (in yoctoNEAR) to send together with the call - */ -function functionCall(methodName, args, gas, deposit) { - const anyArgs = args; - const isUint8Array = anyArgs.byteLength !== undefined && anyArgs.byteLength === anyArgs.length; - const serializedArgs = isUint8Array ? args : Buffer.from(JSON.stringify(args)); - return new Action({ functionCall: new FunctionCall({ methodName, args: serializedArgs, gas, deposit }) }); -} -exports.functionCall = functionCall; -function transfer(deposit) { - return new Action({ transfer: new Transfer({ deposit }) }); -} -exports.transfer = transfer; -function stake(stake, publicKey) { - return new Action({ stake: new Stake({ stake, publicKey }) }); -} -exports.stake = stake; -function addKey(publicKey, accessKey) { - return new Action({ addKey: new AddKey({ publicKey, accessKey }) }); -} -exports.addKey = addKey; -function deleteKey(publicKey) { - return new Action({ deleteKey: new DeleteKey({ publicKey }) }); -} -exports.deleteKey = deleteKey; -function deleteAccount(beneficiaryId) { - return new Action({ deleteAccount: new DeleteAccount({ beneficiaryId }) }); -} -exports.deleteAccount = deleteAccount; -class Signature extends enums_1.Assignable { -} -exports.Signature = Signature; -class Transaction extends enums_1.Assignable { - encode() { - return serialize_1.serialize(exports.SCHEMA, this); - } - static decode(bytes) { - return serialize_1.deserialize(exports.SCHEMA, Transaction, bytes); - } -} -exports.Transaction = Transaction; -class SignedTransaction extends enums_1.Assignable { - encode() { - return serialize_1.serialize(exports.SCHEMA, this); - } - static decode(bytes) { - return serialize_1.deserialize(exports.SCHEMA, SignedTransaction, bytes); - } -} -exports.SignedTransaction = SignedTransaction; -/** - * Contains a list of the valid transaction Actions available with this API - */ -class Action extends enums_1.Enum { -} -exports.Action = Action; -exports.SCHEMA = new Map([ - [Signature, { kind: 'struct', fields: [ - ['keyType', 'u8'], - ['data', [64]] - ] }], - [SignedTransaction, { kind: 'struct', fields: [ - ['transaction', Transaction], - ['signature', Signature] - ] }], - [Transaction, { kind: 'struct', fields: [ - ['signerId', 'string'], - ['publicKey', key_pair_1.PublicKey], - ['nonce', 'u64'], - ['receiverId', 'string'], - ['blockHash', [32]], - ['actions', [Action]] - ] }], - [key_pair_1.PublicKey, { kind: 'struct', fields: [ - ['keyType', 'u8'], - ['data', [32]] - ] }], - [AccessKey, { kind: 'struct', fields: [ - ['nonce', 'u64'], - ['permission', AccessKeyPermission], - ] }], - [AccessKeyPermission, { kind: 'enum', field: 'enum', values: [ - ['functionCall', FunctionCallPermission], - ['fullAccess', FullAccessPermission], - ] }], - [FunctionCallPermission, { kind: 'struct', fields: [ - ['allowance', { kind: 'option', type: 'u128' }], - ['receiverId', 'string'], - ['methodNames', ['string']], - ] }], - [FullAccessPermission, { kind: 'struct', fields: [] }], - [Action, { kind: 'enum', field: 'enum', values: [ - ['createAccount', CreateAccount], - ['deployContract', DeployContract], - ['functionCall', FunctionCall], - ['transfer', Transfer], - ['stake', Stake], - ['addKey', AddKey], - ['deleteKey', DeleteKey], - ['deleteAccount', DeleteAccount], - ] }], - [CreateAccount, { kind: 'struct', fields: [] }], - [DeployContract, { kind: 'struct', fields: [ - ['code', ['u8']] - ] }], - [FunctionCall, { kind: 'struct', fields: [ - ['methodName', 'string'], - ['args', ['u8']], - ['gas', 'u64'], - ['deposit', 'u128'] - ] }], - [Transfer, { kind: 'struct', fields: [ - ['deposit', 'u128'] - ] }], - [Stake, { kind: 'struct', fields: [ - ['stake', 'u128'], - ['publicKey', key_pair_1.PublicKey] - ] }], - [AddKey, { kind: 'struct', fields: [ - ['publicKey', key_pair_1.PublicKey], - ['accessKey', AccessKey] - ] }], - [DeleteKey, { kind: 'struct', fields: [ - ['publicKey', key_pair_1.PublicKey] - ] }], - [DeleteAccount, { kind: 'struct', fields: [ - ['beneficiaryId', 'string'] - ] }], -]); -function createTransaction(signerId, publicKey, receiverId, nonce, actions, blockHash) { - return new Transaction({ signerId, publicKey, nonce, receiverId, actions, blockHash }); -} -exports.createTransaction = createTransaction; -/** - * Signs a given transaction from an account with given keys, applied to the given network - * @param transaction The Transaction object to sign - * @param signer The {Signer} object that assists with signing keys - * @param accountId The human-readable NEAR account name - * @param networkId The targeted network. (ex. default, betanet, etc…) - */ -async function signTransactionObject(transaction, signer, accountId, networkId) { - const message = serialize_1.serialize(exports.SCHEMA, transaction); - const hash = new Uint8Array(js_sha256_1.default.sha256.array(message)); - const signature = await signer.signMessage(message, accountId, networkId); - const signedTx = new SignedTransaction({ - transaction, - signature: new Signature({ keyType: transaction.publicKey.keyType, data: signature.signature }) - }); - return [hash, signedTx]; -} -async function signTransaction(...args) { - if (args[0].constructor === Transaction) { - const [transaction, signer, accountId, networkId] = args; - return signTransactionObject(transaction, signer, accountId, networkId); - } - else { - const [receiverId, nonce, actions, blockHash, signer, accountId, networkId] = args; - const publicKey = await signer.getPublicKey(accountId, networkId); - const transaction = createTransaction(accountId, publicKey, receiverId, nonce, actions, blockHash); - return signTransactionObject(transaction, signer, accountId, networkId); - } -} -exports.signTransaction = signTransaction; diff --git a/borsh-ts/test/serialize.test.js b/borsh-ts/test/serialize.test.js deleted file mode 100644 index 39419ab0f..000000000 --- a/borsh-ts/test/serialize.test.js +++ /dev/null @@ -1,204 +0,0 @@ -const borsh = require('../../lib/index'); -const BN = require('bn.js'); - -class Assignable { - constructor(properties) { - Object.keys(properties).map((key) => { - this[key] = properties[key]; - }); - } -} - -class Test extends Assignable { } - -class Serializable { - constructor(data) { - this.data = data; - } - - static borshDeserialize(reader) { - return new Serializable(reader.readU8()); - } - - borshSerialize(writer) { - writer.writeU8(this.data); - } -} - -test('serialize object', async () => { - const value = new Test({ x: 255, y: 20, z: '123', q: [1, 2, 3] }); - const schema = new Map([[Test, { kind: 'struct', fields: [['x', 'u8'], ['y', 'u64'], ['z', 'string'], ['q', [3]]] }]]); - const buf = borsh.serialize(schema, value); - const newValue = borsh.deserialize(schema, Test, buf); - expect(newValue.x).toEqual(255); - expect(newValue.y.toString()).toEqual('20'); - expect(newValue.z).toEqual('123'); - expect(newValue.q).toEqual(new Uint8Array([1, 2, 3])); -}); - -test('serialize optional field', async () => { - const schema = new Map([[Test, { kind: 'struct', fields: [['x', { kind: 'option', type: 'string' }]] }]]); - - let buf = borsh.serialize(schema, new Test({ x: '123', })); - let newValue = borsh.deserialize(schema, Test, buf); - expect(newValue.x).toEqual('123'); - - buf = borsh.serialize(schema, new Test({})); - newValue = borsh.deserialize(schema, Test, buf); - expect(newValue.x).toEqual(undefined); -}); - -test('serialize max uint', async () => { - const u64MaxHex = 'ffffffffffffffff'; - const value = new Test({ - x: 255, - y: 65535, - z: 4294967295, - q: new BN(u64MaxHex, 16), - r: new BN(u64MaxHex.repeat(2), 16), - s: new BN(u64MaxHex.repeat(4), 16), - t: new BN(u64MaxHex.repeat(8), 16) - }); - const schema = new Map([[Test, { - kind: 'struct', - fields: [ - ['x', 'u8'], - ['y', 'u16'], - ['z', 'u32'], - ['q', 'u64'], - ['r', 'u128'], - ['s', 'u256'], - ['t', 'u512'] - ] - }]]); - const buf = borsh.serialize(schema, value); - const newValue = borsh.deserialize(schema, Test, buf); - expect(newValue.x).toEqual(255); - expect(newValue.y).toEqual(65535); - expect(newValue.z).toEqual(4294967295); - expect(newValue.q.toString()).toEqual('18446744073709551615'); - expect(newValue.r.toString()).toEqual('340282366920938463463374607431768211455'); - expect(newValue.s.toString()).toEqual('115792089237316195423570985008687907853269984665640564039457584007913129639935'); - expect(newValue.t.toString()).toEqual('13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084095'); -}); - -test('serialize/deserialize with class methods', () => { - const item = new Serializable(10); - - const buf = borsh.serialize(null, item); - const newValue = borsh.deserialize(null, Serializable, buf); - - expect(newValue).toEqual(item); -}); - -test('serialize/deserialize fixed array', () => { - const value = new Test({ - a: ['hello', 'world'] - }); - const schema = new Map([[Test, { - kind: 'struct', - fields: [ - ['a', ['string', 2]] - ] - }]]); - - const buf = borsh.serialize(schema, value); - const deserializedValue = borsh.deserialize(schema, Test, buf); - - expect(buf).toEqual(Buffer.from([5, 0, 0, 0, 104, 101, 108, 108, 111, 5, 0, 0, 0, 119, 111, 114, 108, 100])); - expect(deserializedValue.a).toEqual(['hello', 'world']); -}); - -test('errors serializing fixed array of wrong size', () => { - const value = new Test({ - a: ['hello', 'world', 'you'] - }); - const schema = new Map([[Test, { - kind: 'struct', - fields: [ - ['a', ['string', 2]] - ] - }]]); - - expect(() => borsh.serialize(schema, value)).toThrow('Expecting byte array of length 2, but got 3 bytes'); -}); - -test('errors serializing fixed array of wrong type', () => { - const value = new Test({ - a: [244, 34] - }); - const schema = new Map([[Test, { - kind: 'struct', - fields: [ - ['a', ['string', 2]] - ] - }]]); - - expect(() => borsh.serialize(schema, value)).toThrow('The first argument must be of type string'); -}); - -test('baseEncode string test', async () => { - const encodedValue = borsh.baseEncode('244ZQ9cgj3CQ6bWBdytfrJMuMQ1jdXLFGnr4HhvtCTnM'); - const expectedValue = 'HKk9gqNj4xb4rLdJuzT5zzJbLa4vHBdYCxQT9H99csQh6nz3Hfpqn4jtWA92'; - expect(encodedValue).toEqual(expectedValue); -}); - -test('baseEncode array test', async () => { - expect(borsh.baseEncode([1, 2, 3, 4, 5])).toEqual('7bWpTW'); -}); - -test('baseDecode test', async () => { - const value = 'HKk9gqNj4xb4rLdJu'; - const expectedDecodedArray = [3, 96, 254, 84, 10, 240, 93, 199, 52, 244, 164, 240, 6]; - const expectedBuffer = Buffer.from(expectedDecodedArray); - expect(borsh.baseDecode(value)).toEqual(expectedBuffer); -}); - -test('base encode and decode test', async () => { - const value = '244ZQ9cgj3CQ6bWBdytfrJMuMQ1jdXLFGnr4HhvtCTnM'; - expect(borsh.baseEncode(borsh.baseDecode(value))).toEqual(value); -}); - -test('serialize with custom writer/reader', async () => { - class ExtendedWriter extends borsh.BinaryWriter { - writeDate(value) { - this.writeU64(value.getTime()); - } - } - - class ExtendedReader extends borsh.BinaryReader { - readDate() { - const value = this.readU64(); - return new Date(value.toNumber()); - } - } - - const time = 'Aug 12, 2021 12:00:00 UTC+00:00'; - const value = new Test({ x: new Date(time) }); - const schema = new Map([[Test, { kind: 'struct', fields: [['x', 'date']] }]]); - - const buf = borsh.serialize(schema, value, ExtendedWriter); - const newValue = borsh.deserialize(schema, Test, buf, ExtendedReader); - expect(newValue.x).toEqual(new Date(time)); -}); - -test('serialize map', async () => { - let map = new Map(); - for (let i = 0; i < 10; i++) { - map.set(new BN(i * 10), 'some string ' + i.toString()); - } - const value = new Test({ x: map }); - const schema = new Map([[Test, { - kind: 'struct', - fields: [ - ['x', { kind: 'map', key: 'u64', value: 'string' }], - ], - }]]); - - const buf = borsh.serialize(schema, value); - const deserialized = borsh.deserialize(schema, Test, buf); - expect(deserialized.x.size).toEqual(10); - deserialized.x.forEach((value, key) => { - expect(value).toEqual('some string ' + (key.toNumber() / 10).toString()); - }); -}); diff --git a/borsh-ts/test/structures.js b/borsh-ts/test/structures.js new file mode 100644 index 000000000..1351bd265 --- /dev/null +++ b/borsh-ts/test/structures.js @@ -0,0 +1,136 @@ +// Complex number structure +const Numbers = { + u8: 1, + u16: 2, + u32: 3, + u64: 4n, + u128: 5n, + i8: -1, + i16: -2, + i32: -3, + i64: -4n, + f32: 6.0, + f64: 7.1, +}; + +const schemaNumbers = { + struct: { + u8: 'u8', u16: 'u16', u32: 'u32', u64: 'u64', u128: 'u128', i8: 'i8', + i16: 'i16', i32: 'i32', i64: 'i64', f32: 'f32', f64: 'f64' + } +}; + +const encodedNumbers = [ + 1, 2, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 255, 254, 255, 253, 255, 255, 255, 252, 255, 255, 255, + 255, 255, 255, 255, 0, 0, 192, 64, 102, 102, 102, 102, 102, 102, 28, 64 +]; + +// Options +const Options = { + u32: 2, + option: null, + u8: 1, +}; + +const schemaOptions = { + struct: { + u32: { option: 'u32' }, option: { option: 'string' }, u8: { option: 'u8' } + } +}; + +const encodedOptions = [1, 2, 0, 0, 0, 0, 1, 1]; + +// Nested structure +const Nested = { + a: { sa: { n: 1 } }, + b: 2, + c: 3, +}; + +const schemaNested = { + struct: { a: { struct: { sa: { struct: { n: 'u8' } } } }, b: 'u16', c: 'u32' } +}; + +const encodedNested = [1, 2, 0, 3, 0, 0, 0]; + + +// Complex mixture of types +const Mixture = { + foo: 321, + bar: 123, + u64Val: BigInt('4294967297'), + i64Val: -64n, + flag: true, + baz: 'testing', + uint8array: [240, 241], + arr: [['testing'], ['testing']], + u32Arr: [21, 11], + i32Arr: [], + u128Val: 128n, + uint8arrays: [[240, 241], [240, 241]], + u64Arr: [BigInt('10000000000'), 100000000000n], +}; + +const schemaMixture = { + struct: { + foo: 'u32', + bar: 'i32', + u64Val: 'u64', + i64Val: 'i64', + flag: 'bool', + baz: 'string', + uint8array: { array: { type: 'u8', len: 2 } }, + arr: { array: { type: { array: { type: 'string' } } } }, + u32Arr: { array: { type: 'u32' } }, + i32Arr: { array: { type: 'i32' } }, + u128Val: 'u128', + uint8arrays: { array: { type: { array: { type: 'u8', len: 2 } } } }, + u64Arr: { array: { type: 'u64' } }, + } +}; + +const encodedMixture = [ + // i32, u32, u64val, + 65, 1, 0, 0, 123, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + // i64val, B, + 192, 255, 255, 255, 255, 255, 255, 255, 1, + // string, u8array, + 7, 0, 0, 0, 116, 101, 115, 116, 105, 110, 103, 240, 241, + // Array> + 2, 0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, 116, 101, 115, 116, + 105, 110, 103, 1, 0, 0, 0, 7, 0, 0, 0, 116, 101, 115, 116, 105, 110, 103, + // u32Arr, i32Arr, + 2, 0, 0, 0, 21, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, + // u128, + 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // Array, + 2, 0, 0, 0, 240, 241, 240, 241, + // u64Arr + 2, 0, 0, 0, 0, 228, 11, 84, 2, 0, 0, 0, 0, 232, 118, 72, 23, 0, 0, 0 +]; + +// A structure of big nums +const BigStruct = { + u64: BigInt('18446744073709551615'), + u128: BigInt('340282366920938463463374607431768211455'), + arr: [...Array(254).keys()], +}; + +const schemaBigStruct = { + struct: { + u64: 'u64', + u128: 'u128', + arr: { array: { type: 'u8', len: 254 } } + } +}; + +const encodedBigStruct = Array(24).fill(255).concat([...Array(254).keys()]); + +// export module +module.exports = { + Numbers, schemaNumbers, encodedNumbers, Options, schemaOptions, + encodedOptions, Nested, schemaNested, encodedNested, Mixture, + schemaMixture, encodedMixture, BigStruct, schemaBigStruct, + encodedBigStruct +}; \ No newline at end of file diff --git a/borsh-ts/test/utils.test.js b/borsh-ts/test/utils.test.js new file mode 100644 index 000000000..309ca1168 --- /dev/null +++ b/borsh-ts/test/utils.test.js @@ -0,0 +1,44 @@ +const utils = require('../../lib/cjs/utils'); + +test('accept valid schemes', async () => { + const array = { array: { type: 'u8' } }; + const arrayFixed = { array: { type: 'u8', len: 2 } }; + const set = { set: 'u8' }; + const map = { map: { key: 'u8', value: 'u8' } }; + const option = { option: 'u8' }; + const struct = { struct: { u8: 'u8' } }; + const enumeration = { enum: [{ struct: { irrational: 'f32' } }, { struct: { rational: { struct: { num: 'u8', den: 'u8' } } } }] }; + + const valid = [ + array, arrayFixed, set, map, option, enumeration, struct, + 'u8', 'u16', 'u32', 'u64', 'u128', 'i8', 'i16', 'i32', + 'i64', 'i128', 'bool', 'string', 'f32', 'f64' + ]; + + for (const schema of valid) { + expect(() => utils.validate_schema(schema)).not.toThrow(); + } +}); + +test('rejects invalid schemes', async () => { + const array = { array: 'u8' }; + const arrayFixed = { array: { len: 2 } }; + const set = { set: { type: 'u8' } }; + const map = { map: { value: 'u8' } }; + const option = { option: null }; + const struct = { struct: arrayFixed }; + const enumeration = { enum: [ { struct: { num: 'u8', den: 'u8' } } ] }; + const noStruct = { u8: 'u8' }; + + expect(() => utils.validate_schema(array)).toThrow('Invalid schema: "u8" expected { type, len? }'); + expect(() => utils.validate_schema(arrayFixed)).toThrow('Invalid schema: {"len":2} expected { type, len? }'); + expect(() => utils.validate_schema(set)).toThrow('Invalid schema: {"type":"u8"} expected option, enum, array, set, map, struct or u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, bool, string'); + expect(() => utils.validate_schema(map)).toThrow('Invalid schema: {"value":"u8"} expected { key, value }'); + expect(() => utils.validate_schema(option)).toThrow('Invalid schema: null expected option, enum, array, set, map, struct or u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, bool, string'); + expect(() => utils.validate_schema(enumeration)).toThrow('The "struct" in each enum must have a single key'); + expect(() => utils.validate_schema(struct)).toThrow('Invalid schema: {"len":2} expected option, enum, array, set, map, struct or u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, bool, string'); + expect(() => utils.validate_schema(noStruct)).toThrow('Invalid schema: {"u8":"u8"} expected option, enum, array, set, map, struct or u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, bool, string'); + expect(() => utils.validate_schema('u7')).toThrow('Invalid schema: "u7" expected option, enum, array, set, map, struct or u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, bool, string'); + expect(() => utils.validate_schema(Array)).toThrow(); + expect(() => utils.validate_schema(Map)).toThrow(); +}); diff --git a/borsh-ts/types.ts b/borsh-ts/types.ts new file mode 100644 index 000000000..87903ef9c --- /dev/null +++ b/borsh-ts/types.ts @@ -0,0 +1,16 @@ +export const integers = ['u8', 'u16', 'u32', 'u64', 'u128', 'i8', 'i16', 'i32', 'i64', 'i128', 'f32', 'f64']; + +export type IntegerType = typeof integers[number]; +export type BoolType = 'bool'; +export type StringType = 'string'; + +export type OptionType = { option: Schema }; +export type ArrayType = { array: { type: Schema, len?: number } }; +export type EnumType = { enum: Array }; +export type SetType = { set: Schema }; +export type MapType = { map: { key: Schema, value: Schema } }; +export type StructType = { struct: { [key: string]: Schema } }; +export type Schema = IntegerType | BoolType | StringType | OptionType | ArrayType | EnumType | SetType | MapType | StructType; + +// returned +export type DecodeTypes = number | bigint | string | boolean | Array | EnumType | ArrayBuffer | Map | Set | object | null; diff --git a/borsh-ts/utils.ts b/borsh-ts/utils.ts new file mode 100644 index 000000000..778667c41 --- /dev/null +++ b/borsh-ts/utils.ts @@ -0,0 +1,121 @@ +import { Schema, StructType, integers } from './types.js'; + +export function isArrayLike(value: unknown): boolean { + // source: https://stackoverflow.com/questions/24048547/checking-if-an-object-is-array-like + return ( + Array.isArray(value) || + (!!value && + typeof value === 'object' && + 'length' in value && + typeof (value.length) === 'number' && + (value.length === 0 || + (value.length > 0 && + (value.length - 1) in value) + ) + ) + ); +} + +export function expect_type(value: unknown, type: string, fieldPath: string[]): void { + if (typeof (value) !== type) { + throw new Error(`Expected ${type} not ${typeof (value)}(${value}) at ${fieldPath.join('.')}`); + } +} + +export function expect_bigint(value: unknown, fieldPath: string[]): void { + const basicType = ['number', 'string', 'bigint', 'boolean'].includes(typeof(value)); + const strObject = typeof (value) === 'object' && value !== null && 'toString' in value; + if (!basicType && !strObject) { + throw new Error(`Expected bigint, number, boolean or string not ${typeof (value)}(${value}) at ${fieldPath.join('.')}`); + } +} + +export function expect_same_size(length: number, expected: number, fieldPath: string[]): void { + if (length !== expected) { + throw new Error(`Array length ${length} does not match schema length ${expected} at ${fieldPath.join('.')}`); + } +} + +export function expect_enum(value: unknown, fieldPath: string[]): void { + if(typeof (value) !== 'object' || value === null ) { + throw new Error(`Expected object not ${typeof (value)}(${value}) at ${fieldPath.join('.')}`); + } +} + +// Validate Schema +const VALID_STRING_TYPES = integers.concat(['bool', 'string']); +const VALID_OBJECT_KEYS = ['option', 'enum', 'array', 'set', 'map', 'struct']; + +export class ErrorSchema extends Error { + constructor(schema: Schema, expected: string) { + const message = `Invalid schema: ${JSON.stringify(schema)} expected ${expected}`; + super(message); + } +} + +export function validate_schema(schema: Schema): void { + if (typeof (schema) === 'string' && VALID_STRING_TYPES.includes(schema)) { + return; + } + + if (schema && typeof (schema) === 'object') { + const keys = Object.keys(schema); + + if (keys.length === 1 && VALID_OBJECT_KEYS.includes(keys[0])) { + const key = keys[0]; + + if (key === 'option') return validate_schema(schema[key]); + if (key === 'enum') return validate_enum_schema(schema[key]); + if (key === 'array') return validate_array_schema(schema[key]); + if (key === 'set') return validate_schema(schema[key]); + if (key === 'map') return validate_map_schema(schema[key]); + if (key === 'struct') return validate_struct_schema(schema[key]); + } + } + throw new ErrorSchema(schema, VALID_OBJECT_KEYS.join(', ') + ' or ' + VALID_STRING_TYPES.join(', ')); +} + +function validate_enum_schema(schema: Array): void { + if (!Array.isArray(schema)) throw new ErrorSchema(schema, 'Array'); + + for (const sch of schema) { + if (typeof sch !== 'object' || !('struct' in sch)) { + throw new Error('Missing "struct" key in enum schema'); + } + + if (typeof sch.struct !== 'object' || Object.keys(sch.struct).length !== 1) { + throw new Error('The "struct" in each enum must have a single key'); + } + + validate_schema({struct: sch.struct}); + } +} + +function validate_array_schema(schema: { type: Schema, len?: number }): void { + if (typeof schema !== 'object') throw new ErrorSchema(schema, '{ type, len? }'); + + if (schema.len && typeof schema.len !== 'number') { + throw new Error(`Invalid schema: ${schema}`); + } + + if ('type' in schema) return validate_schema(schema.type); + + throw new ErrorSchema(schema, '{ type, len? }'); +} + +function validate_map_schema(schema: { key: Schema, value: Schema }): void { + if (typeof schema === 'object' && 'key' in schema && 'value' in schema) { + validate_schema(schema.key); + validate_schema(schema.value); + } else { + throw new ErrorSchema(schema, '{ key, value }'); + } +} + +function validate_struct_schema(schema: { [key: string]: Schema }): void { + if (typeof schema !== 'object') throw new ErrorSchema(schema, 'object'); + + for (const key in schema) { + validate_schema(schema[key]); + } +} \ No newline at end of file diff --git a/examples/cjs/index.js b/examples/cjs/index.js new file mode 100644 index 000000000..03f08df46 --- /dev/null +++ b/examples/cjs/index.js @@ -0,0 +1,9 @@ +const borsh = require('borsh-js'); + +const encodedU16 = borsh.serialize('u16', 2); +const decodedU16 = borsh.deserialize('u16', encodedU16); +console.log(decodedU16); + +const encodedStr = borsh.serialize('string', 'testing'); +const decodedStr = borsh.deserialize('string', encodedStr); +console.log(decodedStr); diff --git a/examples/cjs/package.json b/examples/cjs/package.json new file mode 100644 index 000000000..5acfda806 --- /dev/null +++ b/examples/cjs/package.json @@ -0,0 +1,10 @@ +{ + "name": "cjs-example", + "private": true, + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "borsh-js": "file:../../" + } +} \ No newline at end of file diff --git a/examples/cjs/tsconfig.json b/examples/cjs/tsconfig.json new file mode 100644 index 000000000..a0e170408 --- /dev/null +++ b/examples/cjs/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2015" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "module": "commonjs" /* Specify what module code is generated. */, + "outDir": "./build" /* Specify an output folder for all emitted files. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "strict": true /* Enable all strict type-checking options. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} \ No newline at end of file diff --git a/examples/esm/index.js b/examples/esm/index.js new file mode 100644 index 000000000..ab745dd75 --- /dev/null +++ b/examples/esm/index.js @@ -0,0 +1,9 @@ +import * as borsh from 'borsh-js'; + +const encodedU16 = borsh.serialize('u16', 2); +const decodedU16 = borsh.deserialize('u16', encodedU16); +console.log(decodedU16); + +const encodedStr = borsh.serialize('u64', '100000000000000000'); +const decodedStr = borsh.deserialize('u64', encodedStr); +console.log(decodedStr); \ No newline at end of file diff --git a/examples/esm/package.json b/examples/esm/package.json new file mode 100644 index 000000000..4a1e776bf --- /dev/null +++ b/examples/esm/package.json @@ -0,0 +1,11 @@ +{ + "name": "esm-example", + "private": true, + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "dependencies": { + "borsh-js": "file:../../" + } +} \ No newline at end of file diff --git a/examples/esm/tsconfig.json b/examples/esm/tsconfig.json new file mode 100644 index 000000000..176365da8 --- /dev/null +++ b/examples/esm/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2015" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "module": "esnext" /* Specify what module code is generated. */, + "outDir": "./build" /* Specify an output folder for all emitted files. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "strict": true /* Enable all strict type-checking options. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} \ No newline at end of file diff --git a/lib/cjs/buffer.d.ts b/lib/cjs/buffer.d.ts new file mode 100644 index 000000000..c850d11e7 --- /dev/null +++ b/lib/cjs/buffer.d.ts @@ -0,0 +1,22 @@ +import { IntegerType } from './types.js'; +export declare class EncodeBuffer { + offset: number; + buffer_size: number; + buffer: ArrayBuffer; + view: DataView; + constructor(); + resize_if_necessary(needed_space: number): void; + get_used_buffer(): Uint8Array; + store_value(value: number, type: IntegerType): void; + store_bytes(from: Uint8Array): void; +} +export declare class DecodeBuffer { + offset: number; + buffer_size: number; + buffer: ArrayBuffer; + view: DataView; + constructor(buf: Uint8Array); + assert_enough_buffer(size: number): void; + consume_value(type: IntegerType): number; + consume_bytes(size: number): ArrayBuffer; +} diff --git a/lib/cjs/buffer.js b/lib/cjs/buffer.js new file mode 100644 index 000000000..e65ce82d2 --- /dev/null +++ b/lib/cjs/buffer.js @@ -0,0 +1,69 @@ +"use strict"; +exports.__esModule = true; +exports.DecodeBuffer = exports.EncodeBuffer = void 0; +var EncodeBuffer = /** @class */ (function () { + function EncodeBuffer() { + this.offset = 0; + this.buffer_size = 256; + this.buffer = new ArrayBuffer(this.buffer_size); + this.view = new DataView(this.buffer); + } + EncodeBuffer.prototype.resize_if_necessary = function (needed_space) { + if (this.buffer_size - this.offset < needed_space) { + this.buffer_size = Math.max(this.buffer_size * 2, this.buffer_size + needed_space); + var new_buffer = new ArrayBuffer(this.buffer_size); + new Uint8Array(new_buffer).set(new Uint8Array(this.buffer)); + this.buffer = new_buffer; + this.view = new DataView(new_buffer); + } + }; + EncodeBuffer.prototype.get_used_buffer = function () { + return new Uint8Array(this.buffer).slice(0, this.offset); + }; + EncodeBuffer.prototype.store_value = function (value, type) { + var bSize = type.substring(1); + var size = parseInt(bSize) / 8; + this.resize_if_necessary(size); + var toCall = type[0] === 'f' ? "setFloat".concat(bSize) : type[0] === 'i' ? "setInt".concat(bSize) : "setUint".concat(bSize); + this.view[toCall](this.offset, value, true); + this.offset += size; + }; + EncodeBuffer.prototype.store_bytes = function (from) { + this.resize_if_necessary(from.length); + new Uint8Array(this.buffer).set(new Uint8Array(from), this.offset); + this.offset += from.length; + }; + return EncodeBuffer; +}()); +exports.EncodeBuffer = EncodeBuffer; +var DecodeBuffer = /** @class */ (function () { + function DecodeBuffer(buf) { + this.offset = 0; + this.buffer_size = buf.length; + this.buffer = new ArrayBuffer(buf.length); + new Uint8Array(this.buffer).set(buf); + this.view = new DataView(this.buffer); + } + DecodeBuffer.prototype.assert_enough_buffer = function (size) { + if (this.offset + size > this.buffer.byteLength) { + throw new Error('Error in schema, the buffer is smaller than expected'); + } + }; + DecodeBuffer.prototype.consume_value = function (type) { + var bSize = type.substring(1); + var size = parseInt(bSize) / 8; + this.assert_enough_buffer(size); + var toCall = type[0] === 'f' ? "getFloat".concat(bSize) : type[0] === 'i' ? "getInt".concat(bSize) : "getUint".concat(bSize); + var ret = this.view[toCall](this.offset, true); + this.offset += size; + return ret; + }; + DecodeBuffer.prototype.consume_bytes = function (size) { + this.assert_enough_buffer(size); + var ret = this.buffer.slice(this.offset, this.offset + size); + this.offset += size; + return ret; + }; + return DecodeBuffer; +}()); +exports.DecodeBuffer = DecodeBuffer; diff --git a/lib/cjs/deserialize.d.ts b/lib/cjs/deserialize.d.ts new file mode 100644 index 000000000..d83685525 --- /dev/null +++ b/lib/cjs/deserialize.d.ts @@ -0,0 +1,18 @@ +import { ArrayType, DecodeTypes, MapType, IntegerType, OptionType, Schema, SetType, StructType, EnumType } from './types.js'; +import { DecodeBuffer } from './buffer.js'; +export declare class BorshDeserializer { + buffer: DecodeBuffer; + constructor(bufferArray: Uint8Array); + decode(schema: Schema): DecodeTypes; + decode_value(schema: Schema): DecodeTypes; + decode_integer(schema: IntegerType): number | bigint; + decode_bigint(size: number, signed?: boolean): bigint; + decode_string(): string; + decode_boolean(): boolean; + decode_option(schema: OptionType): DecodeTypes; + decode_enum(schema: EnumType): DecodeTypes; + decode_array(schema: ArrayType): Array; + decode_set(schema: SetType): Set; + decode_map(schema: MapType): Map; + decode_struct(schema: StructType): object; +} diff --git a/lib/cjs/deserialize.js b/lib/cjs/deserialize.js new file mode 100644 index 000000000..cf6c248a5 --- /dev/null +++ b/lib/cjs/deserialize.js @@ -0,0 +1,118 @@ +"use strict"; +exports.__esModule = true; +exports.BorshDeserializer = void 0; +var types_js_1 = require("./types.js"); +var buffer_js_1 = require("./buffer.js"); +var BorshDeserializer = /** @class */ (function () { + function BorshDeserializer(bufferArray) { + this.buffer = new buffer_js_1.DecodeBuffer(bufferArray); + } + BorshDeserializer.prototype.decode = function (schema) { + return this.decode_value(schema); + }; + BorshDeserializer.prototype.decode_value = function (schema) { + if (typeof schema === 'string') { + if (types_js_1.integers.includes(schema)) + return this.decode_integer(schema); + if (schema === 'string') + return this.decode_string(); + if (schema === 'bool') + return this.decode_boolean(); + } + if (typeof schema === 'object') { + if ('option' in schema) + return this.decode_option(schema); + if ('enum' in schema) + return this.decode_enum(schema); + if ('array' in schema) + return this.decode_array(schema); + if ('set' in schema) + return this.decode_set(schema); + if ('map' in schema) + return this.decode_map(schema); + if ('struct' in schema) + return this.decode_struct(schema); + } + throw new Error("Unsupported type: ".concat(schema)); + }; + BorshDeserializer.prototype.decode_integer = function (schema) { + var size = parseInt(schema.substring(1)); + if (size <= 32 || schema == 'f64') { + return this.buffer.consume_value(schema); + } + return this.decode_bigint(size, schema.startsWith('i')); + }; + BorshDeserializer.prototype.decode_bigint = function (size, signed) { + if (signed === void 0) { signed = false; } + var buffer_len = size / 8; + var buffer = new Uint8Array(this.buffer.consume_bytes(buffer_len)); + var bits = buffer.reduceRight(function (r, x) { return r + x.toString(16).padStart(2, '0'); }, ''); + if (signed && buffer[buffer_len - 1]) { + return BigInt.asIntN(size, BigInt("0x".concat(bits))); + } + return BigInt("0x".concat(bits)); + }; + BorshDeserializer.prototype.decode_string = function () { + var len = this.decode_integer('u32'); + var buffer = new Uint8Array(this.buffer.consume_bytes(len)); + return String.fromCharCode.apply(null, buffer); + }; + BorshDeserializer.prototype.decode_boolean = function () { + return this.buffer.consume_value('u8') > 0; + }; + BorshDeserializer.prototype.decode_option = function (schema) { + var option = this.buffer.consume_value('u8'); + if (option === 1) { + return this.decode_value(schema.option); + } + if (option !== 0) { + throw new Error("Invalid option ".concat(option)); + } + return null; + }; + BorshDeserializer.prototype.decode_enum = function (schema) { + var _a; + var valueIndex = this.buffer.consume_value('u8'); + if (valueIndex > schema["enum"].length) { + throw new Error("Enum option ".concat(valueIndex, " is not available")); + } + var struct = schema["enum"][valueIndex].struct; + var key = Object.keys(struct)[0]; + return _a = {}, _a[key] = this.decode_value(struct[key]), _a; + }; + BorshDeserializer.prototype.decode_array = function (schema) { + var result = []; + var len = schema.array.len ? schema.array.len : this.decode_integer('u32'); + for (var i = 0; i < len; ++i) { + result.push(this.decode_value(schema.array.type)); + } + return result; + }; + BorshDeserializer.prototype.decode_set = function (schema) { + var len = this.decode_integer('u32'); + var result = new Set(); + for (var i = 0; i < len; ++i) { + result.add(this.decode_value(schema.set)); + } + return result; + }; + BorshDeserializer.prototype.decode_map = function (schema) { + var len = this.decode_integer('u32'); + var result = new Map(); + for (var i = 0; i < len; ++i) { + var key = this.decode_value(schema.map.key); + var value = this.decode_value(schema.map.value); + result.set(key, value); + } + return result; + }; + BorshDeserializer.prototype.decode_struct = function (schema) { + var result = {}; + for (var key in schema.struct) { + result[key] = this.decode_value(schema.struct[key]); + } + return result; + }; + return BorshDeserializer; +}()); +exports.BorshDeserializer = BorshDeserializer; diff --git a/lib/cjs/index.d.ts b/lib/cjs/index.d.ts new file mode 100644 index 000000000..29ea18872 --- /dev/null +++ b/lib/cjs/index.d.ts @@ -0,0 +1,4 @@ +import { Schema, DecodeTypes } from './types.js'; +export { Schema } from './types'; +export declare function serialize(schema: Schema, value: unknown, validate?: boolean): Uint8Array; +export declare function deserialize(schema: Schema, buffer: Uint8Array, validate?: boolean): DecodeTypes; diff --git a/lib/cjs/index.js b/lib/cjs/index.js new file mode 100644 index 000000000..a02e4927e --- /dev/null +++ b/lib/cjs/index.js @@ -0,0 +1,45 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +exports.__esModule = true; +exports.deserialize = exports.serialize = void 0; +var serialize_js_1 = require("./serialize.js"); +var deserialize_js_1 = require("./deserialize.js"); +var utils = __importStar(require("./utils.js")); +function serialize(schema, value, validate) { + if (validate === void 0) { validate = true; } + if (validate) + utils.validate_schema(schema); + var serializer = new serialize_js_1.BorshSerializer(validate); + return serializer.encode(value, schema); +} +exports.serialize = serialize; +function deserialize(schema, buffer, validate) { + if (validate === void 0) { validate = true; } + if (validate) + utils.validate_schema(schema); + var deserializer = new deserialize_js_1.BorshDeserializer(buffer); + return deserializer.decode(schema); +} +exports.deserialize = deserialize; diff --git a/lib/cjs/serialize.d.ts b/lib/cjs/serialize.d.ts new file mode 100644 index 000000000..adaaae9a4 --- /dev/null +++ b/lib/cjs/serialize.d.ts @@ -0,0 +1,22 @@ +import { ArrayType, MapType, IntegerType, OptionType, Schema, SetType, StructType, EnumType } from './types.js'; +import { EncodeBuffer } from './buffer.js'; +export declare class BorshSerializer { + encoded: EncodeBuffer; + fieldPath: string[]; + checkTypes: boolean; + constructor(checkTypes: any); + encode(value: unknown, schema: Schema): Uint8Array; + encode_value(value: unknown, schema: Schema): void; + encode_integer(value: unknown, schema: IntegerType): void; + encode_bigint(value: bigint, size: number): void; + encode_string(value: unknown): void; + encode_boolean(value: unknown): void; + encode_option(value: unknown, schema: OptionType): void; + encode_enum(value: unknown, schema: EnumType): void; + encode_array(value: unknown, schema: ArrayType): void; + encode_arraylike(value: ArrayLike, schema: ArrayType): void; + encode_buffer(value: ArrayBuffer, schema: ArrayType): void; + encode_set(value: unknown, schema: SetType): void; + encode_map(value: unknown, schema: MapType): void; + encode_struct(value: unknown, schema: StructType): void; +} diff --git a/lib/cjs/serialize.js b/lib/cjs/serialize.js new file mode 100644 index 000000000..2f47e1b8c --- /dev/null +++ b/lib/cjs/serialize.js @@ -0,0 +1,186 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +exports.__esModule = true; +exports.BorshSerializer = void 0; +var types_js_1 = require("./types.js"); +var buffer_js_1 = require("./buffer.js"); +var utils = __importStar(require("./utils.js")); +var BorshSerializer = /** @class */ (function () { + function BorshSerializer(checkTypes) { + this.encoded = new buffer_js_1.EncodeBuffer(); + this.fieldPath = ['value']; + this.checkTypes = checkTypes; + } + BorshSerializer.prototype.encode = function (value, schema) { + this.encode_value(value, schema); + return this.encoded.get_used_buffer(); + }; + BorshSerializer.prototype.encode_value = function (value, schema) { + if (typeof schema === 'string') { + if (types_js_1.integers.includes(schema)) + return this.encode_integer(value, schema); + if (schema === 'string') + return this.encode_string(value); + if (schema === 'bool') + return this.encode_boolean(value); + } + if (typeof schema === 'object') { + if ('option' in schema) + return this.encode_option(value, schema); + if ('enum' in schema) + return this.encode_enum(value, schema); + if ('array' in schema) + return this.encode_array(value, schema); + if ('set' in schema) + return this.encode_set(value, schema); + if ('map' in schema) + return this.encode_map(value, schema); + if ('struct' in schema) + return this.encode_struct(value, schema); + } + }; + BorshSerializer.prototype.encode_integer = function (value, schema) { + var size = parseInt(schema.substring(1)); + if (size <= 32 || schema == 'f64') { + this.checkTypes && utils.expect_type(value, 'number', this.fieldPath); + this.encoded.store_value(value, schema); + } + else { + this.checkTypes && utils.expect_bigint(value, this.fieldPath); + this.encode_bigint(BigInt(value), size); + } + }; + BorshSerializer.prototype.encode_bigint = function (value, size) { + var buffer_len = size / 8; + var buffer = new Uint8Array(buffer_len); + for (var i = 0; i < buffer_len; i++) { + buffer[i] = Number(value & BigInt(0xff)); + value = value >> BigInt(8); + } + this.encoded.store_bytes(new Uint8Array(buffer)); + }; + BorshSerializer.prototype.encode_string = function (value) { + this.checkTypes && utils.expect_type(value, 'string', this.fieldPath); + var _value = value; + // 4 bytes for length + this.encoded.store_value(_value.length, 'u32'); + // string bytes + for (var i = 0; i < _value.length; i++) { + this.encoded.store_value(_value.charCodeAt(i), 'u8'); + } + }; + BorshSerializer.prototype.encode_boolean = function (value) { + this.checkTypes && utils.expect_type(value, 'boolean', this.fieldPath); + this.encoded.store_value(value ? 1 : 0, 'u8'); + }; + BorshSerializer.prototype.encode_option = function (value, schema) { + if (value === null || value === undefined) { + this.encoded.store_value(0, 'u8'); + } + else { + this.encoded.store_value(1, 'u8'); + this.encode_value(value, schema.option); + } + }; + BorshSerializer.prototype.encode_enum = function (value, schema) { + this.checkTypes && utils.expect_enum(value, this.fieldPath); + var valueKey = Object.keys(value)[0]; + for (var i = 0; i < schema["enum"].length; i++) { + var valueSchema = schema["enum"][i]; + if (valueKey === Object.keys(valueSchema.struct)[0]) { + this.encoded.store_value(i, 'u8'); + return this.encode_struct(value, valueSchema); + } + } + throw new Error("Enum key (".concat(valueKey, ") not found in enum schema: ").concat(JSON.stringify(schema), " at ").concat(this.fieldPath.join('.'))); + }; + BorshSerializer.prototype.encode_array = function (value, schema) { + if (utils.isArrayLike(value)) + return this.encode_arraylike(value, schema); + if (value instanceof ArrayBuffer) + return this.encode_buffer(value, schema); + throw new Error("Expected Array-like not ".concat(typeof (value), "(").concat(value, ") at ").concat(this.fieldPath.join('.'))); + }; + BorshSerializer.prototype.encode_arraylike = function (value, schema) { + if (schema.array.len) { + utils.expect_same_size(value.length, schema.array.len, this.fieldPath); + } + else { + // 4 bytes for length + this.encoded.store_value(value.length, 'u32'); + } + // array values + for (var i = 0; i < value.length; i++) { + this.encode_value(value[i], schema.array.type); + } + }; + BorshSerializer.prototype.encode_buffer = function (value, schema) { + if (schema.array.len) { + utils.expect_same_size(value.byteLength, schema.array.len, this.fieldPath); + } + else { + // 4 bytes for length + this.encoded.store_value(value.byteLength, 'u32'); + } + // array values + this.encoded.store_bytes(new Uint8Array(value)); + }; + BorshSerializer.prototype.encode_set = function (value, schema) { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + var isSet = value instanceof Set; + var values = isSet ? Array.from(value.values()) : Object.values(value); + // 4 bytes for length + this.encoded.store_value(values.length, 'u32'); + // set values + for (var _i = 0, values_1 = values; _i < values_1.length; _i++) { + var value_1 = values_1[_i]; + this.encode_value(value_1, schema.set); + } + }; + BorshSerializer.prototype.encode_map = function (value, schema) { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + var isMap = value instanceof Map; + var keys = isMap ? Array.from(value.keys()) : Object.keys(value); + // 4 bytes for length + this.encoded.store_value(keys.length, 'u32'); + // store key/values + for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) { + var key = keys_1[_i]; + this.encode_value(key, schema.map.key); + this.encode_value(isMap ? value.get(key) : value[key], schema.map.value); + } + }; + BorshSerializer.prototype.encode_struct = function (value, schema) { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + for (var _i = 0, _a = Object.keys(schema.struct); _i < _a.length; _i++) { + var key = _a[_i]; + this.fieldPath.push(key); + this.encode_value(value[key], schema.struct[key]); + this.fieldPath.pop(); + } + }; + return BorshSerializer; +}()); +exports.BorshSerializer = BorshSerializer; diff --git a/lib/cjs/types.d.ts b/lib/cjs/types.d.ts new file mode 100644 index 000000000..548259813 --- /dev/null +++ b/lib/cjs/types.d.ts @@ -0,0 +1,32 @@ +export declare const integers: string[]; +export type IntegerType = typeof integers[number]; +export type BoolType = 'bool'; +export type StringType = 'string'; +export type OptionType = { + option: Schema; +}; +export type ArrayType = { + array: { + type: Schema; + len?: number; + }; +}; +export type EnumType = { + enum: Array; +}; +export type SetType = { + set: Schema; +}; +export type MapType = { + map: { + key: Schema; + value: Schema; + }; +}; +export type StructType = { + struct: { + [key: string]: Schema; + }; +}; +export type Schema = IntegerType | BoolType | StringType | OptionType | ArrayType | EnumType | SetType | MapType | StructType; +export type DecodeTypes = number | bigint | string | boolean | Array | EnumType | ArrayBuffer | Map | Set | object | null; diff --git a/lib/cjs/types.js b/lib/cjs/types.js new file mode 100644 index 000000000..d1bc892ab --- /dev/null +++ b/lib/cjs/types.js @@ -0,0 +1,4 @@ +"use strict"; +exports.__esModule = true; +exports.integers = void 0; +exports.integers = ['u8', 'u16', 'u32', 'u64', 'u128', 'i8', 'i16', 'i32', 'i64', 'i128', 'f32', 'f64']; diff --git a/lib/cjs/utils.d.ts b/lib/cjs/utils.d.ts new file mode 100644 index 000000000..3047d77b9 --- /dev/null +++ b/lib/cjs/utils.d.ts @@ -0,0 +1,10 @@ +import { Schema } from './types.js'; +export declare function isArrayLike(value: unknown): boolean; +export declare function expect_type(value: unknown, type: string, fieldPath: string[]): void; +export declare function expect_bigint(value: unknown, fieldPath: string[]): void; +export declare function expect_same_size(length: number, expected: number, fieldPath: string[]): void; +export declare function expect_enum(value: unknown, fieldPath: string[]): void; +export declare class ErrorSchema extends Error { + constructor(schema: Schema, expected: string); +} +export declare function validate_schema(schema: Schema): void; diff --git a/lib/cjs/utils.js b/lib/cjs/utils.js new file mode 100644 index 000000000..317783f92 --- /dev/null +++ b/lib/cjs/utils.js @@ -0,0 +1,134 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +exports.__esModule = true; +exports.validate_schema = exports.ErrorSchema = exports.expect_enum = exports.expect_same_size = exports.expect_bigint = exports.expect_type = exports.isArrayLike = void 0; +var types_js_1 = require("./types.js"); +function isArrayLike(value) { + // source: https://stackoverflow.com/questions/24048547/checking-if-an-object-is-array-like + return (Array.isArray(value) || + (!!value && + typeof value === 'object' && + 'length' in value && + typeof (value.length) === 'number' && + (value.length === 0 || + (value.length > 0 && + (value.length - 1) in value)))); +} +exports.isArrayLike = isArrayLike; +function expect_type(value, type, fieldPath) { + if (typeof (value) !== type) { + throw new Error("Expected ".concat(type, " not ").concat(typeof (value), "(").concat(value, ") at ").concat(fieldPath.join('.'))); + } +} +exports.expect_type = expect_type; +function expect_bigint(value, fieldPath) { + var basicType = ['number', 'string', 'bigint', 'boolean'].includes(typeof (value)); + var strObject = typeof (value) === 'object' && value !== null && 'toString' in value; + if (!basicType && !strObject) { + throw new Error("Expected bigint, number, boolean or string not ".concat(typeof (value), "(").concat(value, ") at ").concat(fieldPath.join('.'))); + } +} +exports.expect_bigint = expect_bigint; +function expect_same_size(length, expected, fieldPath) { + if (length !== expected) { + throw new Error("Array length ".concat(length, " does not match schema length ").concat(expected, " at ").concat(fieldPath.join('.'))); + } +} +exports.expect_same_size = expect_same_size; +function expect_enum(value, fieldPath) { + if (typeof (value) !== 'object' || value === null) { + throw new Error("Expected object not ".concat(typeof (value), "(").concat(value, ") at ").concat(fieldPath.join('.'))); + } +} +exports.expect_enum = expect_enum; +// Validate Schema +var VALID_STRING_TYPES = types_js_1.integers.concat(['bool', 'string']); +var VALID_OBJECT_KEYS = ['option', 'enum', 'array', 'set', 'map', 'struct']; +var ErrorSchema = /** @class */ (function (_super) { + __extends(ErrorSchema, _super); + function ErrorSchema(schema, expected) { + var message = "Invalid schema: ".concat(JSON.stringify(schema), " expected ").concat(expected); + return _super.call(this, message) || this; + } + return ErrorSchema; +}(Error)); +exports.ErrorSchema = ErrorSchema; +function validate_schema(schema) { + if (typeof (schema) === 'string' && VALID_STRING_TYPES.includes(schema)) { + return; + } + if (schema && typeof (schema) === 'object') { + var keys = Object.keys(schema); + if (keys.length === 1 && VALID_OBJECT_KEYS.includes(keys[0])) { + var key = keys[0]; + if (key === 'option') + return validate_schema(schema[key]); + if (key === 'enum') + return validate_enum_schema(schema[key]); + if (key === 'array') + return validate_array_schema(schema[key]); + if (key === 'set') + return validate_schema(schema[key]); + if (key === 'map') + return validate_map_schema(schema[key]); + if (key === 'struct') + return validate_struct_schema(schema[key]); + } + } + throw new ErrorSchema(schema, VALID_OBJECT_KEYS.join(', ') + ' or ' + VALID_STRING_TYPES.join(', ')); +} +exports.validate_schema = validate_schema; +function validate_enum_schema(schema) { + if (!Array.isArray(schema)) + throw new ErrorSchema(schema, 'Array'); + for (var _i = 0, schema_1 = schema; _i < schema_1.length; _i++) { + var sch = schema_1[_i]; + if (typeof sch !== 'object' || !('struct' in sch)) { + throw new Error('Missing "struct" key in enum schema'); + } + if (typeof sch.struct !== 'object' || Object.keys(sch.struct).length !== 1) { + throw new Error('The "struct" in each enum must have a single key'); + } + validate_schema({ struct: sch.struct }); + } +} +function validate_array_schema(schema) { + if (typeof schema !== 'object') + throw new ErrorSchema(schema, '{ type, len? }'); + if (schema.len && typeof schema.len !== 'number') { + throw new Error("Invalid schema: ".concat(schema)); + } + if ('type' in schema) + return validate_schema(schema.type); + throw new ErrorSchema(schema, '{ type, len? }'); +} +function validate_map_schema(schema) { + if (typeof schema === 'object' && 'key' in schema && 'value' in schema) { + validate_schema(schema.key); + validate_schema(schema.value); + } + else { + throw new ErrorSchema(schema, '{ key, value }'); + } +} +function validate_struct_schema(schema) { + if (typeof schema !== 'object') + throw new ErrorSchema(schema, 'object'); + for (var key in schema) { + validate_schema(schema[key]); + } +} diff --git a/lib/esm/buffer.d.ts b/lib/esm/buffer.d.ts new file mode 100644 index 000000000..c850d11e7 --- /dev/null +++ b/lib/esm/buffer.d.ts @@ -0,0 +1,22 @@ +import { IntegerType } from './types.js'; +export declare class EncodeBuffer { + offset: number; + buffer_size: number; + buffer: ArrayBuffer; + view: DataView; + constructor(); + resize_if_necessary(needed_space: number): void; + get_used_buffer(): Uint8Array; + store_value(value: number, type: IntegerType): void; + store_bytes(from: Uint8Array): void; +} +export declare class DecodeBuffer { + offset: number; + buffer_size: number; + buffer: ArrayBuffer; + view: DataView; + constructor(buf: Uint8Array); + assert_enough_buffer(size: number): void; + consume_value(type: IntegerType): number; + consume_bytes(size: number): ArrayBuffer; +} diff --git a/lib/esm/buffer.js b/lib/esm/buffer.js new file mode 100644 index 000000000..d244a1fbe --- /dev/null +++ b/lib/esm/buffer.js @@ -0,0 +1,66 @@ +var EncodeBuffer = /** @class */ (function () { + function EncodeBuffer() { + this.offset = 0; + this.buffer_size = 256; + this.buffer = new ArrayBuffer(this.buffer_size); + this.view = new DataView(this.buffer); + } + EncodeBuffer.prototype.resize_if_necessary = function (needed_space) { + if (this.buffer_size - this.offset < needed_space) { + this.buffer_size = Math.max(this.buffer_size * 2, this.buffer_size + needed_space); + var new_buffer = new ArrayBuffer(this.buffer_size); + new Uint8Array(new_buffer).set(new Uint8Array(this.buffer)); + this.buffer = new_buffer; + this.view = new DataView(new_buffer); + } + }; + EncodeBuffer.prototype.get_used_buffer = function () { + return new Uint8Array(this.buffer).slice(0, this.offset); + }; + EncodeBuffer.prototype.store_value = function (value, type) { + var bSize = type.substring(1); + var size = parseInt(bSize) / 8; + this.resize_if_necessary(size); + var toCall = type[0] === 'f' ? "setFloat".concat(bSize) : type[0] === 'i' ? "setInt".concat(bSize) : "setUint".concat(bSize); + this.view[toCall](this.offset, value, true); + this.offset += size; + }; + EncodeBuffer.prototype.store_bytes = function (from) { + this.resize_if_necessary(from.length); + new Uint8Array(this.buffer).set(new Uint8Array(from), this.offset); + this.offset += from.length; + }; + return EncodeBuffer; +}()); +export { EncodeBuffer }; +var DecodeBuffer = /** @class */ (function () { + function DecodeBuffer(buf) { + this.offset = 0; + this.buffer_size = buf.length; + this.buffer = new ArrayBuffer(buf.length); + new Uint8Array(this.buffer).set(buf); + this.view = new DataView(this.buffer); + } + DecodeBuffer.prototype.assert_enough_buffer = function (size) { + if (this.offset + size > this.buffer.byteLength) { + throw new Error('Error in schema, the buffer is smaller than expected'); + } + }; + DecodeBuffer.prototype.consume_value = function (type) { + var bSize = type.substring(1); + var size = parseInt(bSize) / 8; + this.assert_enough_buffer(size); + var toCall = type[0] === 'f' ? "getFloat".concat(bSize) : type[0] === 'i' ? "getInt".concat(bSize) : "getUint".concat(bSize); + var ret = this.view[toCall](this.offset, true); + this.offset += size; + return ret; + }; + DecodeBuffer.prototype.consume_bytes = function (size) { + this.assert_enough_buffer(size); + var ret = this.buffer.slice(this.offset, this.offset + size); + this.offset += size; + return ret; + }; + return DecodeBuffer; +}()); +export { DecodeBuffer }; diff --git a/lib/esm/deserialize.d.ts b/lib/esm/deserialize.d.ts new file mode 100644 index 000000000..d83685525 --- /dev/null +++ b/lib/esm/deserialize.d.ts @@ -0,0 +1,18 @@ +import { ArrayType, DecodeTypes, MapType, IntegerType, OptionType, Schema, SetType, StructType, EnumType } from './types.js'; +import { DecodeBuffer } from './buffer.js'; +export declare class BorshDeserializer { + buffer: DecodeBuffer; + constructor(bufferArray: Uint8Array); + decode(schema: Schema): DecodeTypes; + decode_value(schema: Schema): DecodeTypes; + decode_integer(schema: IntegerType): number | bigint; + decode_bigint(size: number, signed?: boolean): bigint; + decode_string(): string; + decode_boolean(): boolean; + decode_option(schema: OptionType): DecodeTypes; + decode_enum(schema: EnumType): DecodeTypes; + decode_array(schema: ArrayType): Array; + decode_set(schema: SetType): Set; + decode_map(schema: MapType): Map; + decode_struct(schema: StructType): object; +} diff --git a/lib/esm/deserialize.js b/lib/esm/deserialize.js new file mode 100644 index 000000000..d1aff9afa --- /dev/null +++ b/lib/esm/deserialize.js @@ -0,0 +1,115 @@ +import { integers } from './types.js'; +import { DecodeBuffer } from './buffer.js'; +var BorshDeserializer = /** @class */ (function () { + function BorshDeserializer(bufferArray) { + this.buffer = new DecodeBuffer(bufferArray); + } + BorshDeserializer.prototype.decode = function (schema) { + return this.decode_value(schema); + }; + BorshDeserializer.prototype.decode_value = function (schema) { + if (typeof schema === 'string') { + if (integers.includes(schema)) + return this.decode_integer(schema); + if (schema === 'string') + return this.decode_string(); + if (schema === 'bool') + return this.decode_boolean(); + } + if (typeof schema === 'object') { + if ('option' in schema) + return this.decode_option(schema); + if ('enum' in schema) + return this.decode_enum(schema); + if ('array' in schema) + return this.decode_array(schema); + if ('set' in schema) + return this.decode_set(schema); + if ('map' in schema) + return this.decode_map(schema); + if ('struct' in schema) + return this.decode_struct(schema); + } + throw new Error("Unsupported type: ".concat(schema)); + }; + BorshDeserializer.prototype.decode_integer = function (schema) { + var size = parseInt(schema.substring(1)); + if (size <= 32 || schema == 'f64') { + return this.buffer.consume_value(schema); + } + return this.decode_bigint(size, schema.startsWith('i')); + }; + BorshDeserializer.prototype.decode_bigint = function (size, signed) { + if (signed === void 0) { signed = false; } + var buffer_len = size / 8; + var buffer = new Uint8Array(this.buffer.consume_bytes(buffer_len)); + var bits = buffer.reduceRight(function (r, x) { return r + x.toString(16).padStart(2, '0'); }, ''); + if (signed && buffer[buffer_len - 1]) { + return BigInt.asIntN(size, BigInt("0x".concat(bits))); + } + return BigInt("0x".concat(bits)); + }; + BorshDeserializer.prototype.decode_string = function () { + var len = this.decode_integer('u32'); + var buffer = new Uint8Array(this.buffer.consume_bytes(len)); + return String.fromCharCode.apply(null, buffer); + }; + BorshDeserializer.prototype.decode_boolean = function () { + return this.buffer.consume_value('u8') > 0; + }; + BorshDeserializer.prototype.decode_option = function (schema) { + var option = this.buffer.consume_value('u8'); + if (option === 1) { + return this.decode_value(schema.option); + } + if (option !== 0) { + throw new Error("Invalid option ".concat(option)); + } + return null; + }; + BorshDeserializer.prototype.decode_enum = function (schema) { + var _a; + var valueIndex = this.buffer.consume_value('u8'); + if (valueIndex > schema["enum"].length) { + throw new Error("Enum option ".concat(valueIndex, " is not available")); + } + var struct = schema["enum"][valueIndex].struct; + var key = Object.keys(struct)[0]; + return _a = {}, _a[key] = this.decode_value(struct[key]), _a; + }; + BorshDeserializer.prototype.decode_array = function (schema) { + var result = []; + var len = schema.array.len ? schema.array.len : this.decode_integer('u32'); + for (var i = 0; i < len; ++i) { + result.push(this.decode_value(schema.array.type)); + } + return result; + }; + BorshDeserializer.prototype.decode_set = function (schema) { + var len = this.decode_integer('u32'); + var result = new Set(); + for (var i = 0; i < len; ++i) { + result.add(this.decode_value(schema.set)); + } + return result; + }; + BorshDeserializer.prototype.decode_map = function (schema) { + var len = this.decode_integer('u32'); + var result = new Map(); + for (var i = 0; i < len; ++i) { + var key = this.decode_value(schema.map.key); + var value = this.decode_value(schema.map.value); + result.set(key, value); + } + return result; + }; + BorshDeserializer.prototype.decode_struct = function (schema) { + var result = {}; + for (var key in schema.struct) { + result[key] = this.decode_value(schema.struct[key]); + } + return result; + }; + return BorshDeserializer; +}()); +export { BorshDeserializer }; diff --git a/lib/esm/index.d.ts b/lib/esm/index.d.ts new file mode 100644 index 000000000..29ea18872 --- /dev/null +++ b/lib/esm/index.d.ts @@ -0,0 +1,4 @@ +import { Schema, DecodeTypes } from './types.js'; +export { Schema } from './types'; +export declare function serialize(schema: Schema, value: unknown, validate?: boolean): Uint8Array; +export declare function deserialize(schema: Schema, buffer: Uint8Array, validate?: boolean): DecodeTypes; diff --git a/lib/esm/index.js b/lib/esm/index.js new file mode 100644 index 000000000..6137b4387 --- /dev/null +++ b/lib/esm/index.js @@ -0,0 +1,17 @@ +import { BorshSerializer } from './serialize.js'; +import { BorshDeserializer } from './deserialize.js'; +import * as utils from './utils.js'; +export function serialize(schema, value, validate) { + if (validate === void 0) { validate = true; } + if (validate) + utils.validate_schema(schema); + var serializer = new BorshSerializer(validate); + return serializer.encode(value, schema); +} +export function deserialize(schema, buffer, validate) { + if (validate === void 0) { validate = true; } + if (validate) + utils.validate_schema(schema); + var deserializer = new BorshDeserializer(buffer); + return deserializer.decode(schema); +} diff --git a/lib/esm/package.json b/lib/esm/package.json new file mode 100644 index 000000000..1632c2c4d --- /dev/null +++ b/lib/esm/package.json @@ -0,0 +1 @@ +{"type": "module"} \ No newline at end of file diff --git a/lib/esm/serialize.d.ts b/lib/esm/serialize.d.ts new file mode 100644 index 000000000..adaaae9a4 --- /dev/null +++ b/lib/esm/serialize.d.ts @@ -0,0 +1,22 @@ +import { ArrayType, MapType, IntegerType, OptionType, Schema, SetType, StructType, EnumType } from './types.js'; +import { EncodeBuffer } from './buffer.js'; +export declare class BorshSerializer { + encoded: EncodeBuffer; + fieldPath: string[]; + checkTypes: boolean; + constructor(checkTypes: any); + encode(value: unknown, schema: Schema): Uint8Array; + encode_value(value: unknown, schema: Schema): void; + encode_integer(value: unknown, schema: IntegerType): void; + encode_bigint(value: bigint, size: number): void; + encode_string(value: unknown): void; + encode_boolean(value: unknown): void; + encode_option(value: unknown, schema: OptionType): void; + encode_enum(value: unknown, schema: EnumType): void; + encode_array(value: unknown, schema: ArrayType): void; + encode_arraylike(value: ArrayLike, schema: ArrayType): void; + encode_buffer(value: ArrayBuffer, schema: ArrayType): void; + encode_set(value: unknown, schema: SetType): void; + encode_map(value: unknown, schema: MapType): void; + encode_struct(value: unknown, schema: StructType): void; +} diff --git a/lib/esm/serialize.js b/lib/esm/serialize.js new file mode 100644 index 000000000..acfd8aeef --- /dev/null +++ b/lib/esm/serialize.js @@ -0,0 +1,160 @@ +import { integers } from './types.js'; +import { EncodeBuffer } from './buffer.js'; +import * as utils from './utils.js'; +var BorshSerializer = /** @class */ (function () { + function BorshSerializer(checkTypes) { + this.encoded = new EncodeBuffer(); + this.fieldPath = ['value']; + this.checkTypes = checkTypes; + } + BorshSerializer.prototype.encode = function (value, schema) { + this.encode_value(value, schema); + return this.encoded.get_used_buffer(); + }; + BorshSerializer.prototype.encode_value = function (value, schema) { + if (typeof schema === 'string') { + if (integers.includes(schema)) + return this.encode_integer(value, schema); + if (schema === 'string') + return this.encode_string(value); + if (schema === 'bool') + return this.encode_boolean(value); + } + if (typeof schema === 'object') { + if ('option' in schema) + return this.encode_option(value, schema); + if ('enum' in schema) + return this.encode_enum(value, schema); + if ('array' in schema) + return this.encode_array(value, schema); + if ('set' in schema) + return this.encode_set(value, schema); + if ('map' in schema) + return this.encode_map(value, schema); + if ('struct' in schema) + return this.encode_struct(value, schema); + } + }; + BorshSerializer.prototype.encode_integer = function (value, schema) { + var size = parseInt(schema.substring(1)); + if (size <= 32 || schema == 'f64') { + this.checkTypes && utils.expect_type(value, 'number', this.fieldPath); + this.encoded.store_value(value, schema); + } + else { + this.checkTypes && utils.expect_bigint(value, this.fieldPath); + this.encode_bigint(BigInt(value), size); + } + }; + BorshSerializer.prototype.encode_bigint = function (value, size) { + var buffer_len = size / 8; + var buffer = new Uint8Array(buffer_len); + for (var i = 0; i < buffer_len; i++) { + buffer[i] = Number(value & BigInt(0xff)); + value = value >> BigInt(8); + } + this.encoded.store_bytes(new Uint8Array(buffer)); + }; + BorshSerializer.prototype.encode_string = function (value) { + this.checkTypes && utils.expect_type(value, 'string', this.fieldPath); + var _value = value; + // 4 bytes for length + this.encoded.store_value(_value.length, 'u32'); + // string bytes + for (var i = 0; i < _value.length; i++) { + this.encoded.store_value(_value.charCodeAt(i), 'u8'); + } + }; + BorshSerializer.prototype.encode_boolean = function (value) { + this.checkTypes && utils.expect_type(value, 'boolean', this.fieldPath); + this.encoded.store_value(value ? 1 : 0, 'u8'); + }; + BorshSerializer.prototype.encode_option = function (value, schema) { + if (value === null || value === undefined) { + this.encoded.store_value(0, 'u8'); + } + else { + this.encoded.store_value(1, 'u8'); + this.encode_value(value, schema.option); + } + }; + BorshSerializer.prototype.encode_enum = function (value, schema) { + this.checkTypes && utils.expect_enum(value, this.fieldPath); + var valueKey = Object.keys(value)[0]; + for (var i = 0; i < schema["enum"].length; i++) { + var valueSchema = schema["enum"][i]; + if (valueKey === Object.keys(valueSchema.struct)[0]) { + this.encoded.store_value(i, 'u8'); + return this.encode_struct(value, valueSchema); + } + } + throw new Error("Enum key (".concat(valueKey, ") not found in enum schema: ").concat(JSON.stringify(schema), " at ").concat(this.fieldPath.join('.'))); + }; + BorshSerializer.prototype.encode_array = function (value, schema) { + if (utils.isArrayLike(value)) + return this.encode_arraylike(value, schema); + if (value instanceof ArrayBuffer) + return this.encode_buffer(value, schema); + throw new Error("Expected Array-like not ".concat(typeof (value), "(").concat(value, ") at ").concat(this.fieldPath.join('.'))); + }; + BorshSerializer.prototype.encode_arraylike = function (value, schema) { + if (schema.array.len) { + utils.expect_same_size(value.length, schema.array.len, this.fieldPath); + } + else { + // 4 bytes for length + this.encoded.store_value(value.length, 'u32'); + } + // array values + for (var i = 0; i < value.length; i++) { + this.encode_value(value[i], schema.array.type); + } + }; + BorshSerializer.prototype.encode_buffer = function (value, schema) { + if (schema.array.len) { + utils.expect_same_size(value.byteLength, schema.array.len, this.fieldPath); + } + else { + // 4 bytes for length + this.encoded.store_value(value.byteLength, 'u32'); + } + // array values + this.encoded.store_bytes(new Uint8Array(value)); + }; + BorshSerializer.prototype.encode_set = function (value, schema) { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + var isSet = value instanceof Set; + var values = isSet ? Array.from(value.values()) : Object.values(value); + // 4 bytes for length + this.encoded.store_value(values.length, 'u32'); + // set values + for (var _i = 0, values_1 = values; _i < values_1.length; _i++) { + var value_1 = values_1[_i]; + this.encode_value(value_1, schema.set); + } + }; + BorshSerializer.prototype.encode_map = function (value, schema) { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + var isMap = value instanceof Map; + var keys = isMap ? Array.from(value.keys()) : Object.keys(value); + // 4 bytes for length + this.encoded.store_value(keys.length, 'u32'); + // store key/values + for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) { + var key = keys_1[_i]; + this.encode_value(key, schema.map.key); + this.encode_value(isMap ? value.get(key) : value[key], schema.map.value); + } + }; + BorshSerializer.prototype.encode_struct = function (value, schema) { + this.checkTypes && utils.expect_type(value, 'object', this.fieldPath); + for (var _i = 0, _a = Object.keys(schema.struct); _i < _a.length; _i++) { + var key = _a[_i]; + this.fieldPath.push(key); + this.encode_value(value[key], schema.struct[key]); + this.fieldPath.pop(); + } + }; + return BorshSerializer; +}()); +export { BorshSerializer }; diff --git a/lib/esm/types.d.ts b/lib/esm/types.d.ts new file mode 100644 index 000000000..548259813 --- /dev/null +++ b/lib/esm/types.d.ts @@ -0,0 +1,32 @@ +export declare const integers: string[]; +export type IntegerType = typeof integers[number]; +export type BoolType = 'bool'; +export type StringType = 'string'; +export type OptionType = { + option: Schema; +}; +export type ArrayType = { + array: { + type: Schema; + len?: number; + }; +}; +export type EnumType = { + enum: Array; +}; +export type SetType = { + set: Schema; +}; +export type MapType = { + map: { + key: Schema; + value: Schema; + }; +}; +export type StructType = { + struct: { + [key: string]: Schema; + }; +}; +export type Schema = IntegerType | BoolType | StringType | OptionType | ArrayType | EnumType | SetType | MapType | StructType; +export type DecodeTypes = number | bigint | string | boolean | Array | EnumType | ArrayBuffer | Map | Set | object | null; diff --git a/lib/esm/types.js b/lib/esm/types.js new file mode 100644 index 000000000..4d4e9331c --- /dev/null +++ b/lib/esm/types.js @@ -0,0 +1 @@ +export var integers = ['u8', 'u16', 'u32', 'u64', 'u128', 'i8', 'i16', 'i32', 'i64', 'i128', 'f32', 'f64']; diff --git a/lib/esm/utils.d.ts b/lib/esm/utils.d.ts new file mode 100644 index 000000000..3047d77b9 --- /dev/null +++ b/lib/esm/utils.d.ts @@ -0,0 +1,10 @@ +import { Schema } from './types.js'; +export declare function isArrayLike(value: unknown): boolean; +export declare function expect_type(value: unknown, type: string, fieldPath: string[]): void; +export declare function expect_bigint(value: unknown, fieldPath: string[]): void; +export declare function expect_same_size(length: number, expected: number, fieldPath: string[]): void; +export declare function expect_enum(value: unknown, fieldPath: string[]): void; +export declare class ErrorSchema extends Error { + constructor(schema: Schema, expected: string); +} +export declare function validate_schema(schema: Schema): void; diff --git a/lib/esm/utils.js b/lib/esm/utils.js new file mode 100644 index 000000000..6e722cbf4 --- /dev/null +++ b/lib/esm/utils.js @@ -0,0 +1,125 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +import { integers } from './types.js'; +export function isArrayLike(value) { + // source: https://stackoverflow.com/questions/24048547/checking-if-an-object-is-array-like + return (Array.isArray(value) || + (!!value && + typeof value === 'object' && + 'length' in value && + typeof (value.length) === 'number' && + (value.length === 0 || + (value.length > 0 && + (value.length - 1) in value)))); +} +export function expect_type(value, type, fieldPath) { + if (typeof (value) !== type) { + throw new Error("Expected ".concat(type, " not ").concat(typeof (value), "(").concat(value, ") at ").concat(fieldPath.join('.'))); + } +} +export function expect_bigint(value, fieldPath) { + var basicType = ['number', 'string', 'bigint', 'boolean'].includes(typeof (value)); + var strObject = typeof (value) === 'object' && value !== null && 'toString' in value; + if (!basicType && !strObject) { + throw new Error("Expected bigint, number, boolean or string not ".concat(typeof (value), "(").concat(value, ") at ").concat(fieldPath.join('.'))); + } +} +export function expect_same_size(length, expected, fieldPath) { + if (length !== expected) { + throw new Error("Array length ".concat(length, " does not match schema length ").concat(expected, " at ").concat(fieldPath.join('.'))); + } +} +export function expect_enum(value, fieldPath) { + if (typeof (value) !== 'object' || value === null) { + throw new Error("Expected object not ".concat(typeof (value), "(").concat(value, ") at ").concat(fieldPath.join('.'))); + } +} +// Validate Schema +var VALID_STRING_TYPES = integers.concat(['bool', 'string']); +var VALID_OBJECT_KEYS = ['option', 'enum', 'array', 'set', 'map', 'struct']; +var ErrorSchema = /** @class */ (function (_super) { + __extends(ErrorSchema, _super); + function ErrorSchema(schema, expected) { + var message = "Invalid schema: ".concat(JSON.stringify(schema), " expected ").concat(expected); + return _super.call(this, message) || this; + } + return ErrorSchema; +}(Error)); +export { ErrorSchema }; +export function validate_schema(schema) { + if (typeof (schema) === 'string' && VALID_STRING_TYPES.includes(schema)) { + return; + } + if (schema && typeof (schema) === 'object') { + var keys = Object.keys(schema); + if (keys.length === 1 && VALID_OBJECT_KEYS.includes(keys[0])) { + var key = keys[0]; + if (key === 'option') + return validate_schema(schema[key]); + if (key === 'enum') + return validate_enum_schema(schema[key]); + if (key === 'array') + return validate_array_schema(schema[key]); + if (key === 'set') + return validate_schema(schema[key]); + if (key === 'map') + return validate_map_schema(schema[key]); + if (key === 'struct') + return validate_struct_schema(schema[key]); + } + } + throw new ErrorSchema(schema, VALID_OBJECT_KEYS.join(', ') + ' or ' + VALID_STRING_TYPES.join(', ')); +} +function validate_enum_schema(schema) { + if (!Array.isArray(schema)) + throw new ErrorSchema(schema, 'Array'); + for (var _i = 0, schema_1 = schema; _i < schema_1.length; _i++) { + var sch = schema_1[_i]; + if (typeof sch !== 'object' || !('struct' in sch)) { + throw new Error('Missing "struct" key in enum schema'); + } + if (typeof sch.struct !== 'object' || Object.keys(sch.struct).length !== 1) { + throw new Error('The "struct" in each enum must have a single key'); + } + validate_schema({ struct: sch.struct }); + } +} +function validate_array_schema(schema) { + if (typeof schema !== 'object') + throw new ErrorSchema(schema, '{ type, len? }'); + if (schema.len && typeof schema.len !== 'number') { + throw new Error("Invalid schema: ".concat(schema)); + } + if ('type' in schema) + return validate_schema(schema.type); + throw new ErrorSchema(schema, '{ type, len? }'); +} +function validate_map_schema(schema) { + if (typeof schema === 'object' && 'key' in schema && 'value' in schema) { + validate_schema(schema.key); + validate_schema(schema.value); + } + else { + throw new ErrorSchema(schema, '{ key, value }'); + } +} +function validate_struct_schema(schema) { + if (typeof schema !== 'object') + throw new ErrorSchema(schema, 'object'); + for (var key in schema) { + validate_schema(schema[key]); + } +} diff --git a/lib/index.d.ts b/lib/index.d.ts deleted file mode 100644 index 326275a7e..000000000 --- a/lib/index.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -/// -import BN from 'bn.js'; -export declare function baseEncode(value: Uint8Array | string): string; -export declare function baseDecode(value: string): Buffer; -export declare type Schema = Map; -export declare class BorshError extends Error { - originalMessage: string; - fieldPath: string[]; - constructor(message: string); - addToFieldPath(fieldName: string): void; -} -export declare class BinaryWriter { - buf: Buffer; - length: number; - constructor(); - maybeResize(): void; - writeU8(value: number): void; - writeU16(value: number): void; - writeU32(value: number): void; - writeU64(value: number | BN): void; - writeU128(value: number | BN): void; - writeU256(value: number | BN): void; - writeU512(value: number | BN): void; - private writeBuffer; - writeString(str: string): void; - writeFixedArray(array: Uint8Array): void; - writeArray(array: any[], fn: any): void; - toArray(): Uint8Array; -} -export declare class BinaryReader { - buf: Buffer; - offset: number; - constructor(buf: Buffer); - readU8(): number; - readU16(): number; - readU32(): number; - readU64(): BN; - readU128(): BN; - readU256(): BN; - readU512(): BN; - private readBuffer; - readString(): string; - readFixedArray(len: number): Uint8Array; - readArray(fn: any): any[]; -} -export declare function serialize(schema: Schema, obj: any, Writer?: typeof BinaryWriter): Uint8Array; -export declare function deserialize(schema: Schema, classType: { - new (args: any): T; -}, buffer: Buffer, Reader?: typeof BinaryReader): T; -export declare function deserializeUnchecked(schema: Schema, classType: { - new (args: any): T; -}, buffer: Buffer, Reader?: typeof BinaryReader): T; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index ec59321f4..000000000 --- a/lib/index.js +++ /dev/null @@ -1,449 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.deserializeUnchecked = exports.deserialize = exports.serialize = exports.BinaryReader = exports.BinaryWriter = exports.BorshError = exports.baseDecode = exports.baseEncode = void 0; -const bn_js_1 = __importDefault(require("bn.js")); -const bs58_1 = __importDefault(require("bs58")); -// TODO: Make sure this polyfill not included when not required -const encoding = __importStar(require("text-encoding-utf-8")); -const ResolvedTextDecoder = typeof TextDecoder !== 'function' ? encoding.TextDecoder : TextDecoder; -const textDecoder = new ResolvedTextDecoder('utf-8', { fatal: true }); -function baseEncode(value) { - if (typeof value === 'string') { - value = Buffer.from(value, 'utf8'); - } - return bs58_1.default.encode(Buffer.from(value)); -} -exports.baseEncode = baseEncode; -function baseDecode(value) { - return Buffer.from(bs58_1.default.decode(value)); -} -exports.baseDecode = baseDecode; -const INITIAL_LENGTH = 1024; -class BorshError extends Error { - originalMessage; - fieldPath = []; - constructor(message) { - super(message); - this.originalMessage = message; - } - addToFieldPath(fieldName) { - this.fieldPath.splice(0, 0, fieldName); - // NOTE: Modifying message directly as jest doesn't use .toString() - this.message = this.originalMessage + ': ' + this.fieldPath.join('.'); - } -} -exports.BorshError = BorshError; -/// Binary encoder. -class BinaryWriter { - buf; - length; - constructor() { - this.buf = Buffer.alloc(INITIAL_LENGTH); - this.length = 0; - } - maybeResize() { - if (this.buf.length < 16 + this.length) { - this.buf = Buffer.concat([this.buf, Buffer.alloc(INITIAL_LENGTH)]); - } - } - writeU8(value) { - this.maybeResize(); - this.buf.writeUInt8(value, this.length); - this.length += 1; - } - writeU16(value) { - this.maybeResize(); - this.buf.writeUInt16LE(value, this.length); - this.length += 2; - } - writeU32(value) { - this.maybeResize(); - this.buf.writeUInt32LE(value, this.length); - this.length += 4; - } - writeU64(value) { - this.maybeResize(); - this.writeBuffer(Buffer.from(new bn_js_1.default(value).toArray('le', 8))); - } - writeU128(value) { - this.maybeResize(); - this.writeBuffer(Buffer.from(new bn_js_1.default(value).toArray('le', 16))); - } - writeU256(value) { - this.maybeResize(); - this.writeBuffer(Buffer.from(new bn_js_1.default(value).toArray('le', 32))); - } - writeU512(value) { - this.maybeResize(); - this.writeBuffer(Buffer.from(new bn_js_1.default(value).toArray('le', 64))); - } - writeBuffer(buffer) { - // Buffer.from is needed as this.buf.subarray can return plain Uint8Array in browser - this.buf = Buffer.concat([ - Buffer.from(this.buf.subarray(0, this.length)), - buffer, - Buffer.alloc(INITIAL_LENGTH), - ]); - this.length += buffer.length; - } - writeString(str) { - this.maybeResize(); - const b = Buffer.from(str, 'utf8'); - this.writeU32(b.length); - this.writeBuffer(b); - } - writeFixedArray(array) { - this.writeBuffer(Buffer.from(array)); - } - writeArray(array, fn) { - this.maybeResize(); - this.writeU32(array.length); - for (const elem of array) { - this.maybeResize(); - fn(elem); - } - } - toArray() { - return this.buf.subarray(0, this.length); - } -} -exports.BinaryWriter = BinaryWriter; -function handlingRangeError(target, propertyKey, propertyDescriptor) { - const originalMethod = propertyDescriptor.value; - propertyDescriptor.value = function (...args) { - try { - return originalMethod.apply(this, args); - } - catch (e) { - if (e instanceof RangeError) { - const code = e.code; - if (['ERR_BUFFER_OUT_OF_BOUNDS', 'ERR_OUT_OF_RANGE'].indexOf(code) >= 0) { - throw new BorshError('Reached the end of buffer when deserializing'); - } - } - throw e; - } - }; -} -class BinaryReader { - buf; - offset; - constructor(buf) { - this.buf = buf; - this.offset = 0; - } - readU8() { - const value = this.buf.readUInt8(this.offset); - this.offset += 1; - return value; - } - readU16() { - const value = this.buf.readUInt16LE(this.offset); - this.offset += 2; - return value; - } - readU32() { - const value = this.buf.readUInt32LE(this.offset); - this.offset += 4; - return value; - } - readU64() { - const buf = this.readBuffer(8); - return new bn_js_1.default(buf, 'le'); - } - readU128() { - const buf = this.readBuffer(16); - return new bn_js_1.default(buf, 'le'); - } - readU256() { - const buf = this.readBuffer(32); - return new bn_js_1.default(buf, 'le'); - } - readU512() { - const buf = this.readBuffer(64); - return new bn_js_1.default(buf, 'le'); - } - readBuffer(len) { - if (this.offset + len > this.buf.length) { - throw new BorshError(`Expected buffer length ${len} isn't within bounds`); - } - const result = this.buf.slice(this.offset, this.offset + len); - this.offset += len; - return result; - } - readString() { - const len = this.readU32(); - const buf = this.readBuffer(len); - try { - // NOTE: Using TextDecoder to fail on invalid UTF-8 - return textDecoder.decode(buf); - } - catch (e) { - throw new BorshError(`Error decoding UTF-8 string: ${e}`); - } - } - readFixedArray(len) { - return new Uint8Array(this.readBuffer(len)); - } - readArray(fn) { - const len = this.readU32(); - const result = Array(); - for (let i = 0; i < len; ++i) { - result.push(fn()); - } - return result; - } -} -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readU8", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readU16", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readU32", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readU64", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readU128", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readU256", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readU512", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readString", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readFixedArray", null); -__decorate([ - handlingRangeError -], BinaryReader.prototype, "readArray", null); -exports.BinaryReader = BinaryReader; -function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); -} -function serializeField(schema, fieldName, value, fieldType, writer) { - try { - // TODO: Handle missing values properly (make sure they never result in just skipped write) - if (typeof fieldType === 'string') { - writer[`write${capitalizeFirstLetter(fieldType)}`](value); - } - else if (fieldType instanceof Array) { - if (typeof fieldType[0] === 'number') { - if (value.length !== fieldType[0]) { - throw new BorshError(`Expecting byte array of length ${fieldType[0]}, but got ${value.length} bytes`); - } - writer.writeFixedArray(value); - } - else if (fieldType.length === 2 && typeof fieldType[1] === 'number') { - if (value.length !== fieldType[1]) { - throw new BorshError(`Expecting byte array of length ${fieldType[1]}, but got ${value.length} bytes`); - } - for (let i = 0; i < fieldType[1]; i++) { - serializeField(schema, null, value[i], fieldType[0], writer); - } - } - else { - writer.writeArray(value, (item) => { - serializeField(schema, fieldName, item, fieldType[0], writer); - }); - } - } - else if (fieldType.kind !== undefined) { - switch (fieldType.kind) { - case 'option': { - if (value === null || value === undefined) { - writer.writeU8(0); - } - else { - writer.writeU8(1); - serializeField(schema, fieldName, value, fieldType.type, writer); - } - break; - } - case 'map': { - writer.writeU32(value.size); - value.forEach((val, key) => { - serializeField(schema, fieldName, key, fieldType.key, writer); - serializeField(schema, fieldName, val, fieldType.value, writer); - }); - break; - } - default: - throw new BorshError(`FieldType ${fieldType} unrecognized`); - } - } - else { - serializeStruct(schema, value, writer); - } - } - catch (error) { - if (error instanceof BorshError) { - error.addToFieldPath(fieldName); - } - throw error; - } -} -function serializeStruct(schema, obj, writer) { - if (typeof obj.borshSerialize === 'function') { - obj.borshSerialize(writer); - return; - } - const structSchema = schema.get(obj.constructor); - if (!structSchema) { - throw new BorshError(`Class ${obj.constructor.name} is missing in schema`); - } - if (structSchema.kind === 'struct') { - structSchema.fields.map(([fieldName, fieldType]) => { - serializeField(schema, fieldName, obj[fieldName], fieldType, writer); - }); - } - else if (structSchema.kind === 'enum') { - const name = obj[structSchema.field]; - for (let idx = 0; idx < structSchema.values.length; ++idx) { - const [fieldName, fieldType] = structSchema.values[idx]; - if (fieldName === name) { - writer.writeU8(idx); - serializeField(schema, fieldName, obj[fieldName], fieldType, writer); - break; - } - } - } - else { - throw new BorshError(`Unexpected schema kind: ${structSchema.kind} for ${obj.constructor.name}`); - } -} -/// Serialize given object using schema of the form: -/// { class_name -> [ [field_name, field_type], .. ], .. } -function serialize(schema, obj, Writer = BinaryWriter) { - const writer = new Writer(); - serializeStruct(schema, obj, writer); - return writer.toArray(); -} -exports.serialize = serialize; -function deserializeField(schema, fieldName, fieldType, reader) { - try { - if (typeof fieldType === 'string') { - return reader[`read${capitalizeFirstLetter(fieldType)}`](); - } - if (fieldType instanceof Array) { - if (typeof fieldType[0] === 'number') { - return reader.readFixedArray(fieldType[0]); - } - else if (typeof fieldType[1] === 'number') { - const arr = []; - for (let i = 0; i < fieldType[1]; i++) { - arr.push(deserializeField(schema, null, fieldType[0], reader)); - } - return arr; - } - else { - return reader.readArray(() => deserializeField(schema, fieldName, fieldType[0], reader)); - } - } - if (fieldType.kind === 'option') { - const option = reader.readU8(); - if (option) { - return deserializeField(schema, fieldName, fieldType.type, reader); - } - return undefined; - } - if (fieldType.kind === 'map') { - const map = new Map(); - const length = reader.readU32(); - for (let i = 0; i < length; i++) { - const key = deserializeField(schema, fieldName, fieldType.key, reader); - const val = deserializeField(schema, fieldName, fieldType.value, reader); - map.set(key, val); - } - return map; - } - return deserializeStruct(schema, fieldType, reader); - } - catch (error) { - if (error instanceof BorshError) { - error.addToFieldPath(fieldName); - } - throw error; - } -} -function deserializeStruct(schema, classType, reader) { - if (typeof classType.borshDeserialize === 'function') { - return classType.borshDeserialize(reader); - } - const structSchema = schema.get(classType); - if (!structSchema) { - throw new BorshError(`Class ${classType.name} is missing in schema`); - } - if (structSchema.kind === 'struct') { - const result = {}; - for (const [fieldName, fieldType] of schema.get(classType).fields) { - result[fieldName] = deserializeField(schema, fieldName, fieldType, reader); - } - return new classType(result); - } - if (structSchema.kind === 'enum') { - const idx = reader.readU8(); - if (idx >= structSchema.values.length) { - throw new BorshError(`Enum index: ${idx} is out of range`); - } - const [fieldName, fieldType] = structSchema.values[idx]; - const fieldValue = deserializeField(schema, fieldName, fieldType, reader); - return new classType({ [fieldName]: fieldValue }); - } - throw new BorshError(`Unexpected schema kind: ${structSchema.kind} for ${classType.constructor.name}`); -} -/// Deserializes object from bytes using schema. -function deserialize(schema, classType, buffer, Reader = BinaryReader) { - const reader = new Reader(buffer); - const result = deserializeStruct(schema, classType, reader); - if (reader.offset < buffer.length) { - throw new BorshError(`Unexpected ${buffer.length - reader.offset} bytes after deserialized data`); - } - return result; -} -exports.deserialize = deserialize; -/// Deserializes object from bytes using schema, without checking the length read -function deserializeUnchecked(schema, classType, buffer, Reader = BinaryReader) { - const reader = new Reader(buffer); - return deserializeStruct(schema, classType, reader); -} -exports.deserializeUnchecked = deserializeUnchecked; diff --git a/lib/types/buffer.d.ts b/lib/types/buffer.d.ts new file mode 100644 index 000000000..c850d11e7 --- /dev/null +++ b/lib/types/buffer.d.ts @@ -0,0 +1,22 @@ +import { IntegerType } from './types.js'; +export declare class EncodeBuffer { + offset: number; + buffer_size: number; + buffer: ArrayBuffer; + view: DataView; + constructor(); + resize_if_necessary(needed_space: number): void; + get_used_buffer(): Uint8Array; + store_value(value: number, type: IntegerType): void; + store_bytes(from: Uint8Array): void; +} +export declare class DecodeBuffer { + offset: number; + buffer_size: number; + buffer: ArrayBuffer; + view: DataView; + constructor(buf: Uint8Array); + assert_enough_buffer(size: number): void; + consume_value(type: IntegerType): number; + consume_bytes(size: number): ArrayBuffer; +} diff --git a/lib/types/deserialize.d.ts b/lib/types/deserialize.d.ts new file mode 100644 index 000000000..d83685525 --- /dev/null +++ b/lib/types/deserialize.d.ts @@ -0,0 +1,18 @@ +import { ArrayType, DecodeTypes, MapType, IntegerType, OptionType, Schema, SetType, StructType, EnumType } from './types.js'; +import { DecodeBuffer } from './buffer.js'; +export declare class BorshDeserializer { + buffer: DecodeBuffer; + constructor(bufferArray: Uint8Array); + decode(schema: Schema): DecodeTypes; + decode_value(schema: Schema): DecodeTypes; + decode_integer(schema: IntegerType): number | bigint; + decode_bigint(size: number, signed?: boolean): bigint; + decode_string(): string; + decode_boolean(): boolean; + decode_option(schema: OptionType): DecodeTypes; + decode_enum(schema: EnumType): DecodeTypes; + decode_array(schema: ArrayType): Array; + decode_set(schema: SetType): Set; + decode_map(schema: MapType): Map; + decode_struct(schema: StructType): object; +} diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts new file mode 100644 index 000000000..29ea18872 --- /dev/null +++ b/lib/types/index.d.ts @@ -0,0 +1,4 @@ +import { Schema, DecodeTypes } from './types.js'; +export { Schema } from './types'; +export declare function serialize(schema: Schema, value: unknown, validate?: boolean): Uint8Array; +export declare function deserialize(schema: Schema, buffer: Uint8Array, validate?: boolean): DecodeTypes; diff --git a/lib/types/serialize.d.ts b/lib/types/serialize.d.ts new file mode 100644 index 000000000..adaaae9a4 --- /dev/null +++ b/lib/types/serialize.d.ts @@ -0,0 +1,22 @@ +import { ArrayType, MapType, IntegerType, OptionType, Schema, SetType, StructType, EnumType } from './types.js'; +import { EncodeBuffer } from './buffer.js'; +export declare class BorshSerializer { + encoded: EncodeBuffer; + fieldPath: string[]; + checkTypes: boolean; + constructor(checkTypes: any); + encode(value: unknown, schema: Schema): Uint8Array; + encode_value(value: unknown, schema: Schema): void; + encode_integer(value: unknown, schema: IntegerType): void; + encode_bigint(value: bigint, size: number): void; + encode_string(value: unknown): void; + encode_boolean(value: unknown): void; + encode_option(value: unknown, schema: OptionType): void; + encode_enum(value: unknown, schema: EnumType): void; + encode_array(value: unknown, schema: ArrayType): void; + encode_arraylike(value: ArrayLike, schema: ArrayType): void; + encode_buffer(value: ArrayBuffer, schema: ArrayType): void; + encode_set(value: unknown, schema: SetType): void; + encode_map(value: unknown, schema: MapType): void; + encode_struct(value: unknown, schema: StructType): void; +} diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts new file mode 100644 index 000000000..548259813 --- /dev/null +++ b/lib/types/types.d.ts @@ -0,0 +1,32 @@ +export declare const integers: string[]; +export type IntegerType = typeof integers[number]; +export type BoolType = 'bool'; +export type StringType = 'string'; +export type OptionType = { + option: Schema; +}; +export type ArrayType = { + array: { + type: Schema; + len?: number; + }; +}; +export type EnumType = { + enum: Array; +}; +export type SetType = { + set: Schema; +}; +export type MapType = { + map: { + key: Schema; + value: Schema; + }; +}; +export type StructType = { + struct: { + [key: string]: Schema; + }; +}; +export type Schema = IntegerType | BoolType | StringType | OptionType | ArrayType | EnumType | SetType | MapType | StructType; +export type DecodeTypes = number | bigint | string | boolean | Array | EnumType | ArrayBuffer | Map | Set | object | null; diff --git a/lib/types/utils.d.ts b/lib/types/utils.d.ts new file mode 100644 index 000000000..3047d77b9 --- /dev/null +++ b/lib/types/utils.d.ts @@ -0,0 +1,10 @@ +import { Schema } from './types.js'; +export declare function isArrayLike(value: unknown): boolean; +export declare function expect_type(value: unknown, type: string, fieldPath: string[]): void; +export declare function expect_bigint(value: unknown, fieldPath: string[]): void; +export declare function expect_same_size(length: number, expected: number, fieldPath: string[]): void; +export declare function expect_enum(value: unknown, fieldPath: string[]): void; +export declare class ErrorSchema extends Error { + constructor(schema: Schema, expected: string); +} +export declare function validate_schema(schema: Schema): void; diff --git a/package.json b/package.json index 0662955f1..3440a114a 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,31 @@ { "name": "borsh", - "version": "0.7.0", + "version": "1.0.0", "description": "Binary Object Representation Serializer for Hashing", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts", "files": [ "lib", "LICENSE-APACHE", "LICENSE-MIT.txt" ], "scripts": { - "build": "tsc -p ./tsconfig.json", "test": "jest test --runInBand", - "fuzz": "jsfuzz borsh-ts/test/fuzz/borsh-roundtrip.js borsh-ts/test/fuzz/corpus/", - "dev": "yarn build -w", "pretest": "yarn build", "lint": "eslint borsh-ts/**/*.ts", - "fix": "eslint borsh-ts/**/*.ts --fix" + "fix": "eslint borsh-ts/**/*.ts --fix", + "compile": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json", + "build:clean": "rm -rf ./lib", + "build": "npm run build:clean && npm run compile && node .build_scripts/prepare-package-json.js" + }, + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "require": "./lib/cjs/index.js", + "import": "./lib/esm/index.js", + "default": "./lib/esm/index.js" + } }, "repository": { "type": "git", @@ -39,21 +48,12 @@ "devDependencies": { "@types/babel__core": "^7.1.2", "@types/babel__template": "^7.0.2", - "@types/bn.js": "^5.1.0", "@types/node": "^12.7.3", "@typescript-eslint/eslint-plugin": "^5.28.0", "@typescript-eslint/parser": "^5.28.0", - "bs58": "^4.0.0", "eslint": "^8.17.0", "jest": "^26.0.1", - "js-sha256": "^0.9.0", - "jsfuzz": "^1.0.14", - "typescript": "^4" - }, - "dependencies": { - "bn.js": "^5.2.0", - "bs58": "^4.0.0", - "buffer": "^6.0.3", - "text-encoding-utf-8": "^1.0.2" + "typescript": "^4", + "bn.js": "^5.2.0" } } diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 000000000..ec2cc5f54 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./lib/cjs", + "module": "commonjs" + } +} \ No newline at end of file diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 000000000..d3cbe916a --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./lib/esm", + "module": "esnext" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index fe2ec2d13..7550b0429 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,7 @@ { "compilerOptions": { "esModuleInterop": true, - "lib": [ - "es2015", - "esnext", - "dom" - ], - "module": "commonjs", - "target": "esnext", "moduleResolution": "node", - "outDir": "./lib", "declaration": true, "preserveSymlinks": true, "preserveWatchOutput": true, diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 000000000..4cc6da97b --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./lib/types", + "declaration": true, + "emitDeclarationOnly": true + } +}