@haneullabs/bcs 2.1.0 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"writer.mjs","names":[],"sources":["../src/writer.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\nimport type { Encoding } from './types.js';\nimport { ulebEncode } from './uleb.js';\nimport { encodeStr } from './utils.js';\n\nexport interface BcsWriterOptions {\n\t/** The initial size (in bytes) of the buffer tht will be allocated */\n\tinitialSize?: number;\n\t/** The maximum size (in bytes) that the buffer is allowed to grow to */\n\tmaxSize?: number;\n\t/** The amount of bytes that will be allocated whenever additional memory is required */\n\tallocateSize?: number;\n}\n\n/**\n * Class used to write BCS data into a buffer. Initializer requires\n * some size of a buffer to init; default value for this buffer is 1KB.\n *\n * Most methods are chainable, so it is possible to write them in one go.\n *\n * @example\n * let serialized = new BcsWriter()\n * .write8(10)\n * .write32(1000000)\n * .write64(10000001000000)\n * .hex();\n */\n\n/**\n * Set of methods that allows data encoding/decoding as standalone\n * BCS value or a part of a composed structure/vector.\n */\nexport class BcsWriter {\n\tprivate dataView: DataView<ArrayBuffer>;\n\tprivate bytePosition: number = 0;\n\tprivate size: number;\n\tprivate maxSize: number;\n\tprivate allocateSize: number;\n\n\tconstructor({\n\t\tinitialSize = 1024,\n\t\tmaxSize = Infinity,\n\t\tallocateSize = 1024,\n\t}: BcsWriterOptions = {}) {\n\t\tthis.size = initialSize;\n\t\tthis.maxSize = maxSize;\n\t\tthis.allocateSize = allocateSize;\n\t\tthis.dataView = new DataView(new ArrayBuffer(initialSize));\n\t}\n\n\tprivate ensureSizeOrGrow(bytes: number) {\n\t\tconst requiredSize = this.bytePosition + bytes;\n\t\tif (requiredSize > this.size) {\n\t\t\tconst nextSize = Math.min(\n\t\t\t\tthis.maxSize,\n\t\t\t\tMath.max(this.size + requiredSize, this.size + this.allocateSize),\n\t\t\t);\n\t\t\tif (requiredSize > nextSize) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Attempting to serialize to BCS, but buffer does not have enough size. Allocated size: ${this.size}, Max size: ${this.maxSize}, Required size: ${requiredSize}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.size = nextSize;\n\t\t\tconst nextBuffer = new ArrayBuffer(this.size);\n\t\t\tnew Uint8Array(nextBuffer).set(new Uint8Array(this.dataView.buffer));\n\t\t\tthis.dataView = new DataView(nextBuffer);\n\t\t}\n\t}\n\n\t/**\n\t * Shift current cursor position by `bytes`.\n\t *\n\t * @param {Number} bytes Number of bytes to\n\t * @returns {this} Self for possible chaining.\n\t */\n\tshift(bytes: number): this {\n\t\tthis.bytePosition += bytes;\n\t\treturn this;\n\t}\n\t/**\n\t * Write a U8 value into a buffer and shift cursor position by 1.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite8(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(1);\n\t\tthis.dataView.setUint8(this.bytePosition, Number(value));\n\t\treturn this.shift(1);\n\t}\n\n\t/**\n\t * Write a U8 value into a buffer and shift cursor position by 1.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twriteBytes(bytes: Uint8Array): this {\n\t\tthis.ensureSizeOrGrow(bytes.length);\n\n\t\tfor (let i = 0; i < bytes.length; i++) {\n\t\t\tthis.dataView.setUint8(this.bytePosition + i, bytes[i]);\n\t\t}\n\n\t\treturn this.shift(bytes.length);\n\t}\n\t/**\n\t * Write a U16 value into a buffer and shift cursor position by 2.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite16(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(2);\n\t\tthis.dataView.setUint16(this.bytePosition, Number(value), true);\n\t\treturn this.shift(2);\n\t}\n\t/**\n\t * Write a U32 value into a buffer and shift cursor position by 4.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite32(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(4);\n\t\tthis.dataView.setUint32(this.bytePosition, Number(value), true);\n\t\treturn this.shift(4);\n\t}\n\t/**\n\t * Write a U64 value into a buffer and shift cursor position by 8.\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite64(value: number | bigint): this {\n\t\ttoLittleEndian(BigInt(value), 8).forEach((el) => this.write8(el));\n\n\t\treturn this;\n\t}\n\t/**\n\t * Write a U128 value into a buffer and shift cursor position by 16.\n\t *\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite128(value: number | bigint): this {\n\t\ttoLittleEndian(BigInt(value), 16).forEach((el) => this.write8(el));\n\n\t\treturn this;\n\t}\n\t/**\n\t * Write a U256 value into a buffer and shift cursor position by 16.\n\t *\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite256(value: number | bigint): this {\n\t\ttoLittleEndian(BigInt(value), 32).forEach((el) => this.write8(el));\n\n\t\treturn this;\n\t}\n\t/**\n\t * Write a ULEB value into a buffer and shift cursor position by number of bytes\n\t * written.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twriteULEB(value: number): this {\n\t\tulebEncode(value).forEach((el) => this.write8(el));\n\t\treturn this;\n\t}\n\t/**\n\t * Write a vector into a buffer by first writing the vector length and then calling\n\t * a callback on each passed value.\n\t *\n\t * @param {Array<Any>} vector Array of elements to write.\n\t * @param {WriteVecCb} cb Callback to call on each element of the vector.\n\t * @returns {this}\n\t */\n\twriteVec(vector: any[], cb: (writer: BcsWriter, el: any, i: number, len: number) => void): this {\n\t\tthis.writeULEB(vector.length);\n\t\tArray.from(vector).forEach((el, i) => cb(this, el, i, vector.length));\n\t\treturn this;\n\t}\n\n\t/**\n\t * Adds support for iterations over the object.\n\t * @returns {Uint8Array}\n\t */\n\t// oxlint-disable-next-line require-yields\n\t*[Symbol.iterator](): Iterator<number, Iterable<number>> {\n\t\tfor (let i = 0; i < this.bytePosition; i++) {\n\t\t\tyield this.dataView.getUint8(i);\n\t\t}\n\t\treturn this.toBytes();\n\t}\n\n\t/**\n\t * Get underlying buffer taking only value bytes (in case initial buffer size was bigger).\n\t * @returns {Uint8Array} Resulting bcs.\n\t */\n\ttoBytes(): Uint8Array<ArrayBuffer> {\n\t\treturn new Uint8Array(this.dataView.buffer.slice(0, this.bytePosition));\n\t}\n\n\t/**\n\t * Represent data as 'hex' or 'base64'\n\t * @param encoding Encoding to use: 'base64' or 'hex'\n\t */\n\ttoString(encoding: Encoding): string {\n\t\treturn encodeStr(this.toBytes(), encoding);\n\t}\n}\n\nfunction toLittleEndian(bigint: bigint, size: number) {\n\tconst result = new Uint8Array(size);\n\tlet i = 0;\n\twhile (bigint > 0) {\n\t\tresult[i] = Number(bigint % BigInt(256));\n\t\tbigint = bigint / BigInt(256);\n\t\ti += 1;\n\t}\n\treturn result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkCA,IAAa,YAAb,MAAuB;CAOtB,YAAY,EACX,cAAc,MACd,UAAU,UACV,eAAe,SACM,CAAC,GAAG;sBATK;EAU9B,KAAK,OAAO;EACZ,KAAK,UAAU;EACf,KAAK,eAAe;EACpB,KAAK,WAAW,IAAI,SAAS,IAAI,YAAY,WAAW,CAAC;CAC1D;CAEA,iBAAyB,OAAe;EACvC,MAAM,eAAe,KAAK,eAAe;EACzC,IAAI,eAAe,KAAK,MAAM;GAC7B,MAAM,WAAW,KAAK,IACrB,KAAK,SACL,KAAK,IAAI,KAAK,OAAO,cAAc,KAAK,OAAO,KAAK,YAAY,CACjE;GACA,IAAI,eAAe,UAClB,MAAM,IAAI,MACT,yFAAyF,KAAK,KAAK,cAAc,KAAK,QAAQ,mBAAmB,cAClJ;GAGD,KAAK,OAAO;GACZ,MAAM,aAAa,IAAI,YAAY,KAAK,IAAI;GAC5C,IAAI,WAAW,UAAU,CAAC,CAAC,IAAI,IAAI,WAAW,KAAK,SAAS,MAAM,CAAC;GACnE,KAAK,WAAW,IAAI,SAAS,UAAU;EACxC;CACD;;;;;;;CAQA,MAAM,OAAqB;EAC1B,KAAK,gBAAgB;EACrB,OAAO;CACR;;;;;;CAMA,OAAO,OAA8B;EACpC,KAAK,iBAAiB,CAAC;EACvB,KAAK,SAAS,SAAS,KAAK,cAAc,OAAO,KAAK,CAAC;EACvD,OAAO,KAAK,MAAM,CAAC;CACpB;;;;;;CAOA,WAAW,OAAyB;EACnC,KAAK,iBAAiB,MAAM,MAAM;EAElC,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KACjC,KAAK,SAAS,SAAS,KAAK,eAAe,GAAG,MAAM,EAAE;EAGvD,OAAO,KAAK,MAAM,MAAM,MAAM;CAC/B;;;;;;CAMA,QAAQ,OAA8B;EACrC,KAAK,iBAAiB,CAAC;EACvB,KAAK,SAAS,UAAU,KAAK,cAAc,OAAO,KAAK,GAAG,IAAI;EAC9D,OAAO,KAAK,MAAM,CAAC;CACpB;;;;;;CAMA,QAAQ,OAA8B;EACrC,KAAK,iBAAiB,CAAC;EACvB,KAAK,SAAS,UAAU,KAAK,cAAc,OAAO,KAAK,GAAG,IAAI;EAC9D,OAAO,KAAK,MAAM,CAAC;CACpB;;;;;;CAMA,QAAQ,OAA8B;EACrC,eAAe,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,SAAS,OAAO,KAAK,OAAO,EAAE,CAAC;EAEhE,OAAO;CACR;;;;;;;CAOA,SAAS,OAA8B;EACtC,eAAe,OAAO,KAAK,GAAG,EAAE,CAAC,CAAC,SAAS,OAAO,KAAK,OAAO,EAAE,CAAC;EAEjE,OAAO;CACR;;;;;;;CAOA,SAAS,OAA8B;EACtC,eAAe,OAAO,KAAK,GAAG,EAAE,CAAC,CAAC,SAAS,OAAO,KAAK,OAAO,EAAE,CAAC;EAEjE,OAAO;CACR;;;;;;;CAOA,UAAU,OAAqB;EAC9B,WAAW,KAAK,CAAC,CAAC,SAAS,OAAO,KAAK,OAAO,EAAE,CAAC;EACjD,OAAO;CACR;;;;;;;;;CASA,SAAS,QAAe,IAAwE;EAC/F,KAAK,UAAU,OAAO,MAAM;EAC5B,MAAM,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,MAAM,GAAG,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC;EACpE,OAAO;CACR;;;;;CAOA,EAAE,OAAO,YAAgD;EACxD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,cAAc,KACtC,MAAM,KAAK,SAAS,SAAS,CAAC;EAE/B,OAAO,KAAK,QAAQ;CACrB;;;;;CAMA,UAAmC;EAClC,OAAO,IAAI,WAAW,KAAK,SAAS,OAAO,MAAM,GAAG,KAAK,YAAY,CAAC;CACvE;;;;;CAMA,SAAS,UAA4B;EACpC,OAAO,UAAU,KAAK,QAAQ,GAAG,QAAQ;CAC1C;AACD;AAEA,SAAS,eAAe,QAAgB,MAAc;CACrD,MAAM,SAAS,IAAI,WAAW,IAAI;CAClC,IAAI,IAAI;CACR,OAAO,SAAS,GAAG;EAClB,OAAO,KAAK,OAAO,SAAS,OAAO,GAAG,CAAC;EACvC,SAAS,SAAS,OAAO,GAAG;EAC5B,KAAK;CACN;CACA,OAAO;AACR"}
1
+ {"version":3,"file":"writer.mjs","names":[],"sources":["../src/writer.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\nimport type { Encoding } from './types.js';\nimport { ulebEncode } from './uleb.js';\nimport { encodeStr } from './utils.js';\n\nexport interface BcsWriterOptions {\n\t/** The initial size (in bytes) of the buffer tht will be allocated */\n\tinitialSize?: number;\n\t/** The maximum size (in bytes) that the buffer is allowed to grow to */\n\tmaxSize?: number;\n\t/** The amount of bytes that will be allocated whenever additional memory is required */\n\tallocateSize?: number;\n}\n\n/**\n * Class used to write BCS data into a buffer. Initializer requires\n * some size of a buffer to init; default value for this buffer is 1KB.\n *\n * Most methods are chainable, so it is possible to write them in one go.\n *\n * @example\n * let serialized = new BcsWriter()\n * .write8(10)\n * .write32(1000000)\n * .write64(10000001000000)\n * .hex();\n */\n\n/**\n * Set of methods that allows data encoding/decoding as standalone\n * BCS value or a part of a composed structure/vector.\n */\nexport class BcsWriter {\n\tprivate dataView: DataView<ArrayBuffer>;\n\tprivate bytes: Uint8Array<ArrayBuffer>;\n\tprivate bytePosition: number = 0;\n\tprivate size: number;\n\tprivate maxSize: number;\n\tprivate allocateSize: number;\n\n\tconstructor({\n\t\tinitialSize = 1024,\n\t\tmaxSize = Infinity,\n\t\tallocateSize = 1024,\n\t}: BcsWriterOptions = {}) {\n\t\tthis.size = initialSize;\n\t\tthis.maxSize = maxSize;\n\t\tthis.allocateSize = allocateSize;\n\t\tthis.dataView = new DataView(new ArrayBuffer(initialSize));\n\t\tthis.bytes = new Uint8Array(this.dataView.buffer);\n\t}\n\n\tprivate ensureSizeOrGrow(bytes: number) {\n\t\tconst requiredSize = this.bytePosition + bytes;\n\t\tif (requiredSize > this.size) {\n\t\t\tconst nextSize = Math.min(\n\t\t\t\tthis.maxSize,\n\t\t\t\tMath.max(this.size + requiredSize, this.size + this.allocateSize),\n\t\t\t);\n\t\t\tif (requiredSize > nextSize) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Attempting to serialize to BCS, but buffer does not have enough size. Allocated size: ${this.size}, Max size: ${this.maxSize}, Required size: ${requiredSize}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.size = nextSize;\n\t\t\tconst nextBuffer = new ArrayBuffer(this.size);\n\t\t\tconst nextBytes = new Uint8Array(nextBuffer);\n\t\t\tnextBytes.set(this.bytes);\n\t\t\tthis.dataView = new DataView(nextBuffer);\n\t\t\tthis.bytes = nextBytes;\n\t\t}\n\t}\n\n\t/**\n\t * Shift current cursor position by `bytes`.\n\t *\n\t * @param {Number} bytes Number of bytes to\n\t * @returns {this} Self for possible chaining.\n\t */\n\tshift(bytes: number): this {\n\t\tthis.bytePosition += bytes;\n\t\treturn this;\n\t}\n\t/**\n\t * Write a U8 value into a buffer and shift cursor position by 1.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite8(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(1);\n\t\tthis.bytes[this.bytePosition] = Number(value);\n\t\treturn this.shift(1);\n\t}\n\n\t/**\n\t * Write a U8 value into a buffer and shift cursor position by 1.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twriteBytes(bytes: Uint8Array): this {\n\t\tthis.ensureSizeOrGrow(bytes.length);\n\t\tthis.bytes.set(bytes, this.bytePosition);\n\n\t\treturn this.shift(bytes.length);\n\t}\n\t/**\n\t * Write a U16 value into a buffer and shift cursor position by 2.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite16(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(2);\n\t\tthis.dataView.setUint16(this.bytePosition, Number(value), true);\n\t\treturn this.shift(2);\n\t}\n\t/**\n\t * Write a U32 value into a buffer and shift cursor position by 4.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite32(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(4);\n\t\tthis.dataView.setUint32(this.bytePosition, Number(value), true);\n\t\treturn this.shift(4);\n\t}\n\t/**\n\t * Write a U64 value into a buffer and shift cursor position by 8.\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite64(value: number | bigint): this {\n\t\treturn this.writeBigLittleEndian(BigInt(value), 8);\n\t}\n\t/**\n\t * Write a U128 value into a buffer and shift cursor position by 16.\n\t *\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite128(value: number | bigint): this {\n\t\treturn this.writeBigLittleEndian(BigInt(value), 16);\n\t}\n\t/**\n\t * Write a U256 value into a buffer and shift cursor position by 16.\n\t *\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite256(value: number | bigint): this {\n\t\treturn this.writeBigLittleEndian(BigInt(value), 32);\n\t}\n\n\t// Negative values encode as zero and values that overflow `size` keep their low bytes\n\tprivate writeBigLittleEndian(value: bigint, size: number): this {\n\t\tthis.ensureSizeOrGrow(size);\n\t\tlet remaining = value > 0n ? value : 0n;\n\t\tfor (let offset = 0; offset < size; offset += 8) {\n\t\t\tthis.dataView.setBigUint64(this.bytePosition + offset, BigInt.asUintN(64, remaining), true);\n\t\t\tremaining >>= 64n;\n\t\t}\n\n\t\treturn this.shift(size);\n\t}\n\t/**\n\t * Write a ULEB value into a buffer and shift cursor position by number of bytes\n\t * written.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twriteULEB(value: number): this {\n\t\tif (value >= 0 && Number.isSafeInteger(value)) {\n\t\t\tlet remaining = value;\n\t\t\twhile (remaining >= 0x80) {\n\t\t\t\tthis.write8((remaining % 0x80) | 0x80);\n\t\t\t\tremaining = Math.floor(remaining / 0x80);\n\t\t\t}\n\t\t\treturn this.write8(remaining);\n\t\t}\n\n\t\tulebEncode(value).forEach((el) => this.write8(el));\n\t\treturn this;\n\t}\n\t/**\n\t * Write a vector into a buffer by first writing the vector length and then calling\n\t * a callback on each passed value.\n\t *\n\t * @param {Array<Any>} vector Array of elements to write.\n\t * @param {WriteVecCb} cb Callback to call on each element of the vector.\n\t * @returns {this}\n\t */\n\twriteVec(vector: any[], cb: (writer: BcsWriter, el: any, i: number, len: number) => void): this {\n\t\tthis.writeULEB(vector.length);\n\t\tArray.from(vector).forEach((el, i) => cb(this, el, i, vector.length));\n\t\treturn this;\n\t}\n\n\t/**\n\t * Adds support for iterations over the object.\n\t * @returns {Uint8Array}\n\t */\n\t// oxlint-disable-next-line require-yields\n\t*[Symbol.iterator](): Iterator<number, Iterable<number>> {\n\t\tfor (let i = 0; i < this.bytePosition; i++) {\n\t\t\tyield this.dataView.getUint8(i);\n\t\t}\n\t\treturn this.toBytes();\n\t}\n\n\t/**\n\t * Get underlying buffer taking only value bytes (in case initial buffer size was bigger).\n\t * @returns {Uint8Array} Resulting bcs.\n\t */\n\ttoBytes(): Uint8Array<ArrayBuffer> {\n\t\treturn new Uint8Array(this.dataView.buffer.slice(0, this.bytePosition));\n\t}\n\n\t/**\n\t * Represent data as 'hex' or 'base64'\n\t * @param encoding Encoding to use: 'base64' or 'hex'\n\t */\n\ttoString(encoding: Encoding): string {\n\t\treturn encodeStr(this.toBytes(), encoding);\n\t}\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haneullabs/bcs",
3
- "version": "2.1.0",
3
+ "version": "2.1.3",
4
4
  "description": "BCS - Canonical Binary Serialization implementation for JavaScript",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Haneul Labs <build@haneul-labs.com>",
@@ -44,14 +44,14 @@
44
44
  ],
45
45
  "homepage": "https://sdk.haneul-labs.com/bcs",
46
46
  "devDependencies": {
47
- "@size-limit/preset-small-lib": "^12.1.0",
48
- "size-limit": "^12.1.0",
49
- "typescript": "^6.0.3",
50
- "vitest": "^4.1.8"
47
+ "@size-limit/preset-small-lib": "^13.0.3",
48
+ "size-limit": "^13.0.3",
49
+ "typescript": "^7.0.2",
50
+ "vitest": "^4.1.11"
51
51
  },
52
52
  "dependencies": {
53
- "@scure/base": "^2.2.0",
54
- "@haneullabs/utils": "^0.4.0"
53
+ "@scure/base": "^2.3.0",
54
+ "@haneullabs/utils": "^0.4.2"
55
55
  },
56
56
  "scripts": {
57
57
  "clean": "rm -rf tsconfig.tsbuildinfo ./dist",
package/src/bcs-type.ts CHANGED
@@ -22,6 +22,12 @@ export class BcsType<T, Input = T, const Name extends string = string> {
22
22
  validate: (value: Input) => void;
23
23
  #write: (value: Input, writer: BcsWriter) => void;
24
24
  #serialize: (value: Input, options?: BcsWriterOptions) => Uint8Array<ArrayBuffer>;
25
+ // Set by transform: validates and writes while converting the input only once. Only used
26
+ // while validate is still the one transform created, so a reassigned validate is honoured.
27
+ #validatedWrite: ((value: Input, writer: BcsWriter) => void) | null = null;
28
+ #validatedSerialize:
29
+ ((value: Input, options?: BcsWriterOptions) => Uint8Array<ArrayBuffer>) | null = null;
30
+ #transformValidate: ((value: Input) => void) | null = null;
25
31
 
26
32
  constructor(
27
33
  options: {
@@ -52,11 +58,18 @@ export class BcsType<T, Input = T, const Name extends string = string> {
52
58
  }
53
59
 
54
60
  write(value: Input, writer: BcsWriter) {
61
+ if (this.#validatedWrite && this.validate === this.#transformValidate) {
62
+ this.#validatedWrite(value, writer);
63
+ return;
64
+ }
55
65
  this.validate(value);
56
66
  this.#write(value, writer);
57
67
  }
58
68
 
59
69
  serialize(value: Input, options?: BcsWriterOptions) {
70
+ if (this.#validatedSerialize && this.validate === this.#transformValidate) {
71
+ return new SerializedBcs(this, this.#validatedSerialize(value, options));
72
+ }
60
73
  this.validate(value);
61
74
  return new SerializedBcs(this, this.#serialize(value, options));
62
75
  }
@@ -87,7 +100,7 @@ export class BcsType<T, Input = T, const Name extends string = string> {
87
100
  input?: (val: Input2) => Input;
88
101
  output?: (value: T) => T2;
89
102
  } & BcsTypeOptions<T2, Input2, NewName>) {
90
- return new BcsType<T2, Input2, NewName>({
103
+ const type = new BcsType<T2, Input2, NewName>({
91
104
  name: (name ?? this.name) as NewName,
92
105
  read: (reader) => (output ? output(this.read(reader)) : (this.read(reader) as never)),
93
106
  write: (value, writer) => this.#write(input ? input(value) : (value as never), writer),
@@ -99,6 +112,23 @@ export class BcsType<T, Input = T, const Name extends string = string> {
99
112
  this.validate(input ? input(value) : (value as never));
100
113
  },
101
114
  });
115
+
116
+ // Equivalent to validate() followed by the write above, but calls input() once instead of twice
117
+ type.#transformValidate = type.validate;
118
+ type.#validatedWrite = (value, writer) => {
119
+ validate?.(value);
120
+ const converted = input ? input(value) : (value as never);
121
+ this.validate(converted);
122
+ this.#write(converted, writer);
123
+ };
124
+ type.#validatedSerialize = (value, options) => {
125
+ validate?.(value);
126
+ const converted = input ? input(value) : (value as never);
127
+ this.validate(converted);
128
+ return this.#serialize(converted, options);
129
+ };
130
+
131
+ return type;
102
132
  }
103
133
  }
104
134
 
@@ -253,9 +283,7 @@ export function stringLikeBcsType<const Name extends string = string>({
253
283
  write: (hex, writer) => {
254
284
  const bytes = toBytes(hex);
255
285
  writer.writeULEB(bytes.length);
256
- for (let i = 0; i < bytes.length; i++) {
257
- writer.write8(bytes[i]);
258
- }
286
+ writer.writeBytes(bytes);
259
287
  },
260
288
  serialize: (value) => {
261
289
  const bytes = toBytes(value);
@@ -325,14 +353,16 @@ export class BcsStruct<
325
353
  Name
326
354
  > {
327
355
  constructor({ name, fields, ...options }: BcsStructOptions<T, Name>) {
328
- const canonicalOrder = Object.entries(fields);
356
+ const fieldNames = Object.keys(fields);
357
+ const fieldTypes = fieldNames.map((field) => fields[field]);
358
+ const fieldCount = fieldNames.length;
329
359
 
330
360
  super({
331
361
  name,
332
362
  serializedSize: (values) => {
333
363
  let total = 0;
334
- for (const [field, type] of canonicalOrder) {
335
- const size = type.serializedSize(values[field]);
364
+ for (let i = 0; i < fieldCount; i++) {
365
+ const size = fieldTypes[i].serializedSize(values[fieldNames[i]]);
336
366
  if (size == null) {
337
367
  return null;
338
368
  }
@@ -344,15 +374,15 @@ export class BcsStruct<
344
374
  },
345
375
  read: (reader) => {
346
376
  const result: Record<string, unknown> = {};
347
- for (const [field, type] of canonicalOrder) {
348
- result[field] = type.read(reader);
377
+ for (let i = 0; i < fieldCount; i++) {
378
+ result[fieldNames[i]] = fieldTypes[i].read(reader);
349
379
  }
350
380
 
351
381
  return result as never;
352
382
  },
353
383
  write: (value, writer) => {
354
- for (const [field, type] of canonicalOrder) {
355
- type.write(value[field], writer);
384
+ for (let i = 0; i < fieldCount; i++) {
385
+ fieldTypes[i].write(value[fieldNames[i]], writer);
356
386
  }
357
387
  },
358
388
  ...options,
@@ -399,6 +429,7 @@ export class BcsEnum<
399
429
  > {
400
430
  constructor({ fields, ...options }: BcsEnumOptions<T, Name>) {
401
431
  const canonicalOrder = Object.entries(fields as object);
432
+ const variantIndex = new Map(canonicalOrder.map(([name], i) => [name, i]));
402
433
  super({
403
434
  read: (reader) => {
404
435
  const index = reader.readULEB();
@@ -410,21 +441,26 @@ export class BcsEnum<
410
441
 
411
442
  const [kind, type] = enumEntry;
412
443
 
413
- return {
414
- [kind]: type?.read(reader) ?? true,
415
- $kind: kind,
416
- } as never;
444
+ const value = type?.read(reader) ?? true;
445
+ // Assigning to __proto__ would set the prototype rather than add a property
446
+ if (kind === '__proto__') {
447
+ return { [kind]: value, $kind: kind } as never;
448
+ }
449
+
450
+ // Avoid a computed-key object literal, which V8 builds through a slow runtime path
451
+ const result: Record<string, unknown> = {};
452
+ result[kind] = value;
453
+ result.$kind = kind;
454
+
455
+ return result as never;
417
456
  },
418
457
  write: (value, writer) => {
419
- const [name, val] = Object.entries(value).filter(([name]) =>
420
- Object.hasOwn(fields, name),
421
- )[0];
422
-
423
- for (let i = 0; i < canonicalOrder.length; i++) {
424
- const [optionName, optionType] = canonicalOrder[i];
425
- if (optionName === name) {
426
- writer.writeULEB(i);
427
- optionType?.write(val, writer);
458
+ // The first own key that names a variant selects it
459
+ for (const name of Object.keys(value)) {
460
+ const index = variantIndex.get(name);
461
+ if (index !== undefined) {
462
+ writer.writeULEB(index);
463
+ canonicalOrder[index][1]?.write(value[name], writer);
428
464
  return;
429
465
  }
430
466
  }
@@ -436,21 +472,18 @@ export class BcsEnum<
436
472
  throw new TypeError(`Expected object, found ${typeof value}`);
437
473
  }
438
474
 
439
- const keys = Object.keys(value).filter(
440
- (k) => value[k] !== undefined && Object.hasOwn(fields, k),
441
- );
475
+ let variantCount = 0;
476
+ for (const key of Object.keys(value)) {
477
+ if (value[key] !== undefined && variantIndex.has(key)) {
478
+ variantCount++;
479
+ }
480
+ }
442
481
 
443
- if (keys.length !== 1) {
482
+ if (variantCount !== 1) {
444
483
  throw new TypeError(
445
- `Expected object with one key, but found ${keys.length} for type ${options.name}}`,
484
+ `Expected object with one key, but found ${variantCount} for type ${options.name}}`,
446
485
  );
447
486
  }
448
-
449
- const [variant] = keys;
450
-
451
- if (!Object.hasOwn(fields, variant)) {
452
- throw new TypeError(`Invalid enum variant ${variant}`);
453
- }
454
487
  },
455
488
  });
456
489
  }
package/src/bcs.ts CHANGED
@@ -82,27 +82,34 @@ function option<T, Input, Name extends string = string>(
82
82
  function option<T extends BcsType<any>>(
83
83
  type: T,
84
84
  ): BcsType<InferBcsType<T> | null, InferBcsInput<T> | null | undefined, `Option<${T['name']}>`> {
85
- return bcs
86
- .enum(`Option<${type.name}>`, {
87
- None: null,
88
- Some: type,
89
- })
90
- .transform({
91
- input: (value: InferBcsInput<T> | null | undefined) => {
92
- if (value == null) {
93
- return { None: true };
94
- }
95
-
96
- return { Some: value };
97
- },
98
- output: (value) => {
99
- if (value.$kind === 'Some') {
100
- return value.Some as InferBcsType<T>;
101
- }
102
-
85
+ // Encoded like enum Option { None, Some(T) }, but read and written directly rather than
86
+ // through an intermediate { None } / { Some } enum object
87
+ const name = `Option<${type.name}>` as const;
88
+ return new BcsType<
89
+ InferBcsType<T> | null,
90
+ InferBcsInput<T> | null | undefined,
91
+ `Option<${T['name']}>`
92
+ >({
93
+ name,
94
+ read: (reader) => {
95
+ const index = reader.readULEB();
96
+ if (index === 0) {
103
97
  return null;
104
- },
105
- });
98
+ }
99
+ if (index === 1) {
100
+ return type.read(reader);
101
+ }
102
+ throw new TypeError(`Unknown value ${index} for enum ${name}`);
103
+ },
104
+ write: (value, writer) => {
105
+ if (value == null) {
106
+ writer.write8(0);
107
+ return;
108
+ }
109
+ writer.write8(1);
110
+ type.write(value, writer);
111
+ },
112
+ });
106
113
  }
107
114
 
108
115
  function vector<T extends BcsType<any>, Name extends string = `vector<${T['name']}>`>(
@@ -211,6 +218,32 @@ function map<K extends BcsType<any>, V extends BcsType<any>>(
211
218
  });
212
219
  }
213
220
 
221
+ // Created on first use, so importing the package doesn't require TextEncoder/TextDecoder
222
+ let utf8Encoder: TextEncoder | undefined;
223
+ let utf8Decoder: TextDecoder | undefined;
224
+ function decoder() {
225
+ return (utf8Decoder ??= new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }));
226
+ }
227
+
228
+ // Most strings in Haneul data (module, function and type names) are short ASCII. Decoding those
229
+ // directly avoids the fixed cost of a TextDecoder call; anything else goes through the decoder.
230
+ const ASCII_FAST_PATH_MAX_LENGTH = 64;
231
+ function decodeUtf8(bytes: Uint8Array): string {
232
+ if (bytes.length <= ASCII_FAST_PATH_MAX_LENGTH) {
233
+ let result = '';
234
+ for (let i = 0; i < bytes.length; i++) {
235
+ const byte = bytes[i];
236
+ if (byte >= 0x80) {
237
+ return decoder().decode(bytes);
238
+ }
239
+ result += String.fromCharCode(byte);
240
+ }
241
+ return result;
242
+ }
243
+
244
+ return decoder().decode(bytes);
245
+ }
246
+
214
247
  export const bcs = {
215
248
  /**
216
249
  * Creates a BcsType that can be used to read and write an 8-bit unsigned integer.
@@ -316,7 +349,13 @@ export const bcs = {
316
349
  bool(options?: BcsTypeOptions<boolean>) {
317
350
  return fixedSizeBcsType({
318
351
  size: 1,
319
- read: (reader) => reader.read8() === 1,
352
+ read: (reader) => {
353
+ const value = reader.read8();
354
+ if (value > 1) {
355
+ throw new TypeError(`Invalid bool value: expected 0 or 1, found ${value}`);
356
+ }
357
+ return value === 1;
358
+ },
320
359
  write: (value, writer) => writer.write8(value ? 1 : 0),
321
360
  ...options,
322
361
  name: (options?.name ?? 'bool') as 'bool',
@@ -356,7 +395,7 @@ export const bcs = {
356
395
  size,
357
396
  read: (reader) => reader.readBytes(size),
358
397
  write: (value, writer) => {
359
- writer.writeBytes(new Uint8Array(value));
398
+ writer.writeBytes(value instanceof Uint8Array ? value : new Uint8Array(value));
360
399
  },
361
400
  ...options,
362
401
  name: (options?.name ?? `bytes[${size}]`) as `bytes[${T}]`,
@@ -386,7 +425,7 @@ export const bcs = {
386
425
  return reader.readBytes(length);
387
426
  },
388
427
  write: (value, writer) => {
389
- const array = new Uint8Array(value);
428
+ const array = value instanceof Uint8Array ? value : new Uint8Array(value);
390
429
  writer.writeULEB(array.length);
391
430
  writer.writeBytes(array);
392
431
  },
@@ -412,8 +451,8 @@ export const bcs = {
412
451
  */
413
452
  string(options?: BcsTypeOptions<string>) {
414
453
  return stringLikeBcsType({
415
- toBytes: (value) => new TextEncoder().encode(value),
416
- fromBytes: (bytes) => new TextDecoder().decode(bytes),
454
+ toBytes: (value) => (utf8Encoder ??= new TextEncoder()).encode(value),
455
+ fromBytes: decodeUtf8,
417
456
  ...options,
418
457
  name: (options?.name ?? 'string') as 'string',
419
458
  });
package/src/reader.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  // Copyright (c) Mysten Labs, Inc.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import { ulebDecode } from './uleb.js';
5
-
6
4
  /**
7
5
  * Class used for reading BCS data chunk by chunk. Meant to be used
8
6
  * by some wrapper, which will make sure that data is valid and is
@@ -35,6 +33,7 @@ import { ulebDecode } from './uleb.js';
35
33
  */
36
34
  export class BcsReader {
37
35
  private dataView: DataView;
36
+ private bytes: Uint8Array;
38
37
  private bytePosition: number = 0;
39
38
 
40
39
  /**
@@ -42,6 +41,7 @@ export class BcsReader {
42
41
  */
43
42
  constructor(data: Uint8Array) {
44
43
  this.dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
44
+ this.bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
45
45
  }
46
46
  /**
47
47
  * Shift current cursor position by `bytes`.
@@ -58,9 +58,12 @@ export class BcsReader {
58
58
  * @returns
59
59
  */
60
60
  read8(): number {
61
- const value = this.dataView.getUint8(this.bytePosition);
62
- this.shift(1);
63
- return value;
61
+ const position = this.bytePosition;
62
+ if (position >= this.bytes.length) {
63
+ throw new RangeError('Offset is outside the bounds of the DataView');
64
+ }
65
+ this.bytePosition = position + 1;
66
+ return this.bytes[position];
64
67
  }
65
68
  /**
66
69
  * Read U16 value from the buffer and shift cursor by 2.
@@ -85,12 +88,9 @@ export class BcsReader {
85
88
  * @returns
86
89
  */
87
90
  read64(): string {
88
- const value1 = this.read32();
89
- const value2 = this.read32();
90
-
91
- const result = value2.toString(16) + value1.toString(16).padStart(8, '0');
92
-
93
- return BigInt('0x' + result).toString(10);
91
+ const value = this.dataView.getBigUint64(this.bytePosition, true);
92
+ this.shift(8);
93
+ return value.toString(10);
94
94
  }
95
95
  /**
96
96
  * Read U128 value from the buffer and shift cursor by 16.
@@ -118,6 +118,10 @@ export class BcsReader {
118
118
  * @param num Number of bytes to read.
119
119
  */
120
120
  readBytes(num: number): Uint8Array {
121
+ // The underlying buffer may extend past this view, so check against the view's own length
122
+ if (this.bytePosition + num > this.bytes.length) {
123
+ throw new RangeError('Offset is outside the bounds of the DataView');
124
+ }
121
125
  const start = this.bytePosition + this.dataView.byteOffset;
122
126
  const value = new Uint8Array(this.dataView.buffer, start, num);
123
127
 
@@ -131,12 +135,31 @@ export class BcsReader {
131
135
  * @returns {Number} The ULEB value.
132
136
  */
133
137
  readULEB(): number {
134
- const start = this.bytePosition + this.dataView.byteOffset;
135
- const buffer = new Uint8Array(this.dataView.buffer, start);
136
- const { value, length } = ulebDecode(buffer);
137
-
138
- this.shift(length);
138
+ const bytes = this.bytes;
139
+ let position = this.bytePosition;
140
+ let value = 0;
141
+ let multiplier = 1;
142
+ while (true) {
143
+ if (position >= bytes.length) {
144
+ throw new Error('ULEB decode error: buffer overflow');
145
+ }
146
+ const byte = bytes[position++];
147
+ value += (byte & 0x7f) * multiplier;
148
+ if ((byte & 0x80) === 0) {
149
+ // A trailing zero group adds no value, so the encoding is not minimal
150
+ if (byte === 0 && position - this.bytePosition > 1) {
151
+ throw new Error('ULEB decode error: non-canonical encoding');
152
+ }
153
+ break;
154
+ }
155
+ multiplier *= 128;
156
+ }
139
157
 
158
+ // Also rejects NaN, from very long encodings where multiplier overflows to Infinity
159
+ if (!(value <= Number.MAX_SAFE_INTEGER)) {
160
+ throw new Error('ULEB decode error: value exceeds MAX_SAFE_INTEGER');
161
+ }
162
+ this.bytePosition = position;
140
163
  return value;
141
164
  }
142
165
  /**
package/src/uleb.ts CHANGED
@@ -43,6 +43,10 @@ export function ulebDecode(arr: number[] | Uint8Array): {
43
43
  len += 1;
44
44
  total += BigInt(byte & 0x7f) << shift;
45
45
  if ((byte & 0x80) === 0) {
46
+ // A trailing zero group adds no value, so the encoding is not minimal
47
+ if (byte === 0 && len > 1) {
48
+ throw new Error('ULEB decode error: non-canonical encoding');
49
+ }
46
50
  break;
47
51
  }
48
52
  shift += 7n;
package/src/writer.ts CHANGED
@@ -34,6 +34,7 @@ export interface BcsWriterOptions {
34
34
  */
35
35
  export class BcsWriter {
36
36
  private dataView: DataView<ArrayBuffer>;
37
+ private bytes: Uint8Array<ArrayBuffer>;
37
38
  private bytePosition: number = 0;
38
39
  private size: number;
39
40
  private maxSize: number;
@@ -48,6 +49,7 @@ export class BcsWriter {
48
49
  this.maxSize = maxSize;
49
50
  this.allocateSize = allocateSize;
50
51
  this.dataView = new DataView(new ArrayBuffer(initialSize));
52
+ this.bytes = new Uint8Array(this.dataView.buffer);
51
53
  }
52
54
 
53
55
  private ensureSizeOrGrow(bytes: number) {
@@ -65,8 +67,10 @@ export class BcsWriter {
65
67
 
66
68
  this.size = nextSize;
67
69
  const nextBuffer = new ArrayBuffer(this.size);
68
- new Uint8Array(nextBuffer).set(new Uint8Array(this.dataView.buffer));
70
+ const nextBytes = new Uint8Array(nextBuffer);
71
+ nextBytes.set(this.bytes);
69
72
  this.dataView = new DataView(nextBuffer);
73
+ this.bytes = nextBytes;
70
74
  }
71
75
  }
72
76
 
@@ -87,7 +91,7 @@ export class BcsWriter {
87
91
  */
88
92
  write8(value: number | bigint): this {
89
93
  this.ensureSizeOrGrow(1);
90
- this.dataView.setUint8(this.bytePosition, Number(value));
94
+ this.bytes[this.bytePosition] = Number(value);
91
95
  return this.shift(1);
92
96
  }
93
97
 
@@ -98,10 +102,7 @@ export class BcsWriter {
98
102
  */
99
103
  writeBytes(bytes: Uint8Array): this {
100
104
  this.ensureSizeOrGrow(bytes.length);
101
-
102
- for (let i = 0; i < bytes.length; i++) {
103
- this.dataView.setUint8(this.bytePosition + i, bytes[i]);
104
- }
105
+ this.bytes.set(bytes, this.bytePosition);
105
106
 
106
107
  return this.shift(bytes.length);
107
108
  }
@@ -131,9 +132,7 @@ export class BcsWriter {
131
132
  * @returns {this}
132
133
  */
133
134
  write64(value: number | bigint): this {
134
- toLittleEndian(BigInt(value), 8).forEach((el) => this.write8(el));
135
-
136
- return this;
135
+ return this.writeBigLittleEndian(BigInt(value), 8);
137
136
  }
138
137
  /**
139
138
  * Write a U128 value into a buffer and shift cursor position by 16.
@@ -142,9 +141,7 @@ export class BcsWriter {
142
141
  * @returns {this}
143
142
  */
144
143
  write128(value: number | bigint): this {
145
- toLittleEndian(BigInt(value), 16).forEach((el) => this.write8(el));
146
-
147
- return this;
144
+ return this.writeBigLittleEndian(BigInt(value), 16);
148
145
  }
149
146
  /**
150
147
  * Write a U256 value into a buffer and shift cursor position by 16.
@@ -153,9 +150,19 @@ export class BcsWriter {
153
150
  * @returns {this}
154
151
  */
155
152
  write256(value: number | bigint): this {
156
- toLittleEndian(BigInt(value), 32).forEach((el) => this.write8(el));
153
+ return this.writeBigLittleEndian(BigInt(value), 32);
154
+ }
157
155
 
158
- return this;
156
+ // Negative values encode as zero and values that overflow `size` keep their low bytes
157
+ private writeBigLittleEndian(value: bigint, size: number): this {
158
+ this.ensureSizeOrGrow(size);
159
+ let remaining = value > 0n ? value : 0n;
160
+ for (let offset = 0; offset < size; offset += 8) {
161
+ this.dataView.setBigUint64(this.bytePosition + offset, BigInt.asUintN(64, remaining), true);
162
+ remaining >>= 64n;
163
+ }
164
+
165
+ return this.shift(size);
159
166
  }
160
167
  /**
161
168
  * Write a ULEB value into a buffer and shift cursor position by number of bytes
@@ -164,6 +171,15 @@ export class BcsWriter {
164
171
  * @returns {this}
165
172
  */
166
173
  writeULEB(value: number): this {
174
+ if (value >= 0 && Number.isSafeInteger(value)) {
175
+ let remaining = value;
176
+ while (remaining >= 0x80) {
177
+ this.write8((remaining % 0x80) | 0x80);
178
+ remaining = Math.floor(remaining / 0x80);
179
+ }
180
+ return this.write8(remaining);
181
+ }
182
+
167
183
  ulebEncode(value).forEach((el) => this.write8(el));
168
184
  return this;
169
185
  }
@@ -209,14 +225,3 @@ export class BcsWriter {
209
225
  return encodeStr(this.toBytes(), encoding);
210
226
  }
211
227
  }
212
-
213
- function toLittleEndian(bigint: bigint, size: number) {
214
- const result = new Uint8Array(size);
215
- let i = 0;
216
- while (bigint > 0) {
217
- result[i] = Number(bigint % BigInt(256));
218
- bigint = bigint / BigInt(256);
219
- i += 1;
220
- }
221
- return result;
222
- }