// Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 import { MAX_U128_BIG_INT, MAX_U16_NUMBER, MAX_U32_NUMBER, MAX_U64_BIG_INT, MAX_U8_NUMBER, MAX_U256_BIG_INT, MIN_I8_NUMBER, MAX_I8_NUMBER, MIN_I16_NUMBER, MAX_I16_NUMBER, MIN_I32_NUMBER, MAX_I32_NUMBER, MIN_I64_BIG_INT, MAX_I64_BIG_INT, MIN_I128_BIG_INT, MAX_I128_BIG_INT, MIN_I256_BIG_INT, MAX_I256_BIG_INT, } from "./consts"; import { Hex } from "../core/hex"; import { AnyNumber, Uint16, Uint32, Uint8 } from "../types"; import { TEXT_ENCODER } from "../utils/const"; /** * This class serves as a base class for all serializable types. It facilitates * composable serialization of complex types and enables the serialization of * instances to their BCS (Binary Canonical Serialization) representation. * @group Implementation * @category BCS */ export abstract class Serializable { abstract serialize(serializer: Serializer): void; /** * Serializes a `Serializable` value to its BCS representation. * This function is the TypeScript SDK equivalent of `bcs::to_bytes` in Move. * @returns the BCS representation of the Serializable instance as a byte buffer. * @group Implementation * @category BCS */ bcsToBytes(): Uint8Array { const serializer = new Serializer(); this.serialize(serializer); return serializer.toUint8Array(); } /** * Converts the BCS-serialized bytes of a value into a Hex instance. * This function provides a Hex representation of the BCS-serialized data for easier handling and manipulation. * @returns A Hex instance with the BCS-serialized bytes loaded into its underlying Uint8Array. * @group Implementation * @category BCS */ bcsToHex(): Hex { const bcsBytes = this.bcsToBytes(); return Hex.fromHexInput(bcsBytes); } /** * Returns the hex string representation of the `Serializable` value without the 0x prefix. * @returns the hex format as a string without `0x` prefix. */ toStringWithoutPrefix(): string { return this.bcsToHex().toStringWithoutPrefix(); } /** * Returns the hex string representation of the `Serializable` value with the 0x prefix. * @returns the hex formatas a string prefixed by `0x`. */ toString(): string { return `0x${this.toStringWithoutPrefix()}`; } } /** * Serialize a Serializable value as length-prefixed bytes into a Serializer, * with backwards compatibility for older Serializer implementations that lack * the `serializeAsBytes` method. This is critical for cross-version compatibility * when SDK objects built with a newer SDK are serialized by an older SDK's Serializer * (e.g., wallet extensions bundling an older SDK version). * * @param serializer - The serializer to write into (may be from any SDK version). * @param value - The Serializable value to serialize as bytes. */ export function serializeEntryFunctionBytesCompat(serializer: Serializer, value: Serializable): void { if (typeof (serializer as { serializeAsBytes?: unknown }).serializeAsBytes === "function") { serializer.serializeAsBytes(value); } else { const bcsBytes = value.bcsToBytes(); serializer.serializeBytes(bcsBytes); } } /** * Minimum buffer growth increment to avoid too many small reallocations. */ const MIN_BUFFER_GROWTH = 256; /** * Pool of reusable Serializer instances for temporary serialization operations. * This reduces allocations when using serializeAsBytes() repeatedly. */ const serializerPool: Serializer[] = []; const MAX_POOL_SIZE = 8; /** * Acquires a Serializer from the pool or creates a new one. * @internal */ function acquireSerializer(): Serializer { const serializer = serializerPool.pop(); if (serializer) { serializer.reset(); return serializer; } return new Serializer(); } /** * Returns a Serializer to the pool for reuse. * @internal */ function releaseSerializer(serializer: Serializer): void { if (serializerPool.length < MAX_POOL_SIZE) { serializerPool.push(serializer); } } /** * A class for serializing various data types into a binary format. * It provides methods to serialize strings, bytes, numbers, and other serializable objects * using the Binary Coded Serialization (BCS) layout. The serialized data can be retrieved as a * Uint8Array. * @group Implementation * @category BCS */ export class Serializer { private buffer: ArrayBuffer; private offset: number; /** * Reusable DataView instance to reduce allocations during serialization. * Recreated when buffer is resized. */ private dataView: DataView; /** * Constructs a serializer with a buffer of size `length` bytes, 64 bytes by default. * The `length` must be greater than 0. * * @param length - The size of the buffer in bytes. * @group Implementation * @category BCS */ constructor(length: number = 64) { if (length <= 0) { throw new Error("Length needs to be greater than 0"); } this.buffer = new ArrayBuffer(length); this.dataView = new DataView(this.buffer); this.offset = 0; } /** * Ensures that the internal buffer can accommodate the specified number of bytes. * This function dynamically resizes the buffer using a growth factor of 1.5x with * a minimum growth increment to balance memory usage and reallocation frequency. * * @param bytes - The number of bytes to ensure the buffer can handle. * @group Implementation * @category BCS */ private ensureBufferWillHandleSize(bytes: number) { const requiredSize = this.offset + bytes; if (this.buffer.byteLength >= requiredSize) { return; } // Calculate new size: max of (1.5x current size) or (current + required + MIN_GROWTH) // Using 1.5x instead of 2x provides better memory efficiency const growthSize = Math.max(Math.floor(this.buffer.byteLength * 1.5), requiredSize + MIN_BUFFER_GROWTH); const newBuffer = new ArrayBuffer(growthSize); new Uint8Array(newBuffer).set(new Uint8Array(this.buffer, 0, this.offset)); this.buffer = newBuffer; this.dataView = new DataView(this.buffer); } /** * Appends the specified values to the buffer, ensuring that the buffer can accommodate the new data. * * @param {Uint8Array} values - The values to be appended to the buffer. * @group Implementation * @category BCS */ protected appendToBuffer(values: Uint8Array) { this.ensureBufferWillHandleSize(values.length); new Uint8Array(this.buffer, this.offset).set(values); this.offset += values.length; } /** * Serializes a value into the buffer using the provided function, ensuring the buffer can accommodate the size. * Uses the cached DataView instance for better performance. * * @param fn - The function to serialize the value, which takes a byte offset, the value to serialize, and an optional little-endian flag. * @param fn.byteOffset - The byte offset at which to write the value. * @param fn.value - The numeric value to serialize into the buffer. * @param fn.littleEndian - Optional flag indicating whether to use little-endian byte order (defaults to true). * @group Implementation * @category BCS */ // TODO: JSDoc bytesLength and value private serializeWithFunction( fn: (byteOffset: number, value: number, littleEndian?: boolean) => void, bytesLength: number, value: number, ) { this.ensureBufferWillHandleSize(bytesLength); fn.apply(this.dataView, [this.offset, value, true]); this.offset += bytesLength; } /** * Serializes a string. UTF8 string is supported. * The number of bytes in the string content is serialized first, as a uleb128-encoded u32 integer. * Then the string content is serialized as UTF8 encoded bytes. * * BCS layout for "string": string_length | string_content * where string_length is a u32 integer encoded as a uleb128 integer, equal to the number of bytes in string_content. * * @param value - The string to serialize. * * @example * ```typescript * const serializer = new Serializer(); * serializer.serializeStr("1234abcd"); * assert(serializer.toUint8Array() === new Uint8Array([8, 49, 50, 51, 52, 97, 98, 99, 100])); * ``` * @group Implementation * @category BCS */ serializeStr(value: string) { this.serializeBytes(TEXT_ENCODER.encode(value)); } /** * Serializes an array of bytes. * * This function encodes the length of the byte array as a u32 integer in uleb128 format, followed by the byte array itself. * BCS layout for "bytes": bytes_length | bytes * where bytes_length is a u32 integer encoded as a uleb128 integer, equal to the length of the bytes array. * @param value - The byte array to serialize. * @group Implementation * @category BCS */ serializeBytes(value: Uint8Array) { this.serializeU32AsUleb128(value.length); this.appendToBuffer(value); } /** * Serializes an array of bytes with a known length, allowing for efficient deserialization without needing to serialize the * length itself. * When deserializing, the number of bytes to deserialize needs to be passed in. * @param value - The Uint8Array to be serialized. * @group Implementation * @category BCS */ serializeFixedBytes(value: Uint8Array) { this.appendToBuffer(value); } /** * Serializes a boolean value into a byte representation. * * The BCS layout for a boolean uses one byte, where "0x01" represents true and "0x00" represents false. * * @param value - The boolean value to serialize. * @group Implementation * @category BCS */ serializeBool(value: boolean) { /** * Ensures that the provided value is a boolean. * This function throws an error if the value is not a boolean, helping to enforce type safety in your code. * * @param value - The value to be checked for boolean type. * @throws {Error} Throws an error if the value is not a boolean. * @group Implementation * @category BCS */ ensureBoolean(value); const byteValue = value ? 1 : 0; this.appendToBuffer(new Uint8Array([byteValue])); } /** * Serializes a Uint8 value and appends it to the buffer. * BCS layout for "uint8": One byte. Binary format in little-endian representation. * * @param value - The Uint8 value to serialize. * @group Implementation * @category BCS */ @checkNumberRange(0, MAX_U8_NUMBER) serializeU8(value: Uint8) { this.appendToBuffer(new Uint8Array([value])); } /** * Serializes a uint16 number. * * @group Implementation * @category BCS */ /** * Serializes a 16-bit unsigned integer value into a binary format. * BCS layout for "uint16": Two bytes. Binary format in little-endian representation. * * @param value - The 16-bit unsigned integer value to serialize. * @example * ```typescript * const serializer = new Serializer(); * serializer.serializeU16(4660); * assert(serializer.toUint8Array() === new Uint8Array([0x34, 0x12])); * ``` * @group Implementation * @category BCS */ @checkNumberRange(0, MAX_U16_NUMBER) serializeU16(value: Uint16) { this.serializeWithFunction(DataView.prototype.setUint16, 2, value); } /** * Serializes a 32-bit unsigned integer value into a binary format. * This function is useful for encoding data that needs to be stored or transmitted in a compact form. * @example * ```typescript * const serializer = new Serializer(); * serializer.serializeU32(305419896); * assert(serializer.toUint8Array() === new Uint8Array([0x78, 0x56, 0x34, 0x12])); * ``` * @param value - The 32-bit unsigned integer value to serialize. * @group Implementation * @category BCS */ @checkNumberRange(0, MAX_U32_NUMBER) serializeU32(value: Uint32) { this.serializeWithFunction(DataView.prototype.setUint32, 4, value); } /** * Serializes a 64-bit unsigned integer into a format suitable for storage or transmission. * This function breaks down the value into two 32-bit components and writes them in little-endian order. * * @param value - The 64-bit unsigned integer to serialize, represented as a number. * @example * ```ts * const serializer = new Serializer(); * serializer.serializeU64(1311768467750121216); * assert(serializer.toUint8Array() === new Uint8Array([0x00, 0xEF, 0xCD, 0xAB, 0x78, 0x56, 0x34, 0x12])); * ``` * @group Implementation * @category BCS */ @checkNumberRange(BigInt(0), MAX_U64_BIG_INT) serializeU64(value: AnyNumber) { const low = BigInt(value) & BigInt(MAX_U32_NUMBER); const high = BigInt(value) >> BigInt(32); // write little endian number this.serializeU32(Number(low)); this.serializeU32(Number(high)); } /** * Serializes a U128 value into a format suitable for storage or transmission. * * @param value - The U128 value to serialize, represented as a number. * @group Implementation * @category BCS */ @checkNumberRange(BigInt(0), MAX_U128_BIG_INT) serializeU128(value: AnyNumber) { const low = BigInt(value) & MAX_U64_BIG_INT; const high = BigInt(value) >> BigInt(64); // write little endian number this.serializeU64(low); this.serializeU64(high); } /** * Serializes a U256 value into a byte representation. * This function is essential for encoding large numbers in a compact format suitable for transmission or storage. * * @param value - The U256 value to serialize, represented as an AnyNumber. * @group Implementation * @category BCS */ @checkNumberRange(BigInt(0), MAX_U256_BIG_INT) serializeU256(value: AnyNumber) { const low = BigInt(value) & MAX_U128_BIG_INT; const high = BigInt(value) >> BigInt(128); // write little endian number this.serializeU128(low); this.serializeU128(high); } /** * Serializes an 8-bit signed integer value. * BCS layout for "int8": One byte. Binary format in little-endian representation. * * @param value - The 8-bit signed integer value to serialize. * @group Implementation * @category BCS */ @checkNumberRange(MIN_I8_NUMBER, MAX_I8_NUMBER) serializeI8(value: number) { this.serializeWithFunction(DataView.prototype.setInt8, 1, value); } /** * Serializes a 16-bit signed integer value into a binary format. * BCS layout for "int16": Two bytes. Binary format in little-endian representation. * * @param value - The 16-bit signed integer value to serialize. * @group Implementation * @category BCS */ @checkNumberRange(MIN_I16_NUMBER, MAX_I16_NUMBER) serializeI16(value: number) { this.serializeWithFunction(DataView.prototype.setInt16, 2, value); } /** * Serializes a 32-bit signed integer value into a binary format. * * @param value - The 32-bit signed integer value to serialize. * @group Implementation * @category BCS */ @checkNumberRange(MIN_I32_NUMBER, MAX_I32_NUMBER) serializeI32(value: number) { this.serializeWithFunction(DataView.prototype.setInt32, 4, value); } /** * Serializes a 64-bit signed integer into a format suitable for storage or transmission. * This function uses two's complement representation for negative values. * * @param value - The 64-bit signed integer to serialize. * @group Implementation * @category BCS */ @checkNumberRange(MIN_I64_BIG_INT, MAX_I64_BIG_INT) serializeI64(value: AnyNumber) { const val = BigInt(value); // Convert to unsigned representation using two's complement const unsigned = val < 0 ? (BigInt(1) << BigInt(64)) + val : val; const low = unsigned & BigInt(MAX_U32_NUMBER); const high = unsigned >> BigInt(32); // write little endian number this.serializeU32(Number(low)); this.serializeU32(Number(high)); } /** * Serializes a 128-bit signed integer value. * * @param value - The 128-bit signed integer value to serialize. * @group Implementation * @category BCS */ @checkNumberRange(MIN_I128_BIG_INT, MAX_I128_BIG_INT) serializeI128(value: AnyNumber) { const val = BigInt(value); // Convert to unsigned representation using two's complement const unsigned = val < 0 ? (BigInt(1) << BigInt(128)) + val : val; const low = unsigned & MAX_U64_BIG_INT; const high = unsigned >> BigInt(64); // write little endian number this.serializeU64(low); this.serializeU64(high); } /** * Serializes a 256-bit signed integer value. * * @param value - The 256-bit signed integer value to serialize. * @group Implementation * @category BCS */ @checkNumberRange(MIN_I256_BIG_INT, MAX_I256_BIG_INT) serializeI256(value: AnyNumber) { const val = BigInt(value); // Convert to unsigned representation using two's complement const unsigned = val < 0 ? (BigInt(1) << BigInt(256)) + val : val; const low = unsigned & MAX_U128_BIG_INT; const high = unsigned >> BigInt(128); // write little endian number this.serializeU128(low); this.serializeU128(high); } /** * Serializes a 32-bit unsigned integer as a variable-length ULEB128 encoded byte array. * BCS uses uleb128 encoding in two cases: (1) lengths of variable-length sequences and (2) tags of enum values * * @param val - The 32-bit unsigned integer value to be serialized. * @group Implementation * @category BCS */ @checkNumberRange(0, MAX_U32_NUMBER) serializeU32AsUleb128(val: Uint32) { let value = val; const valueArray = []; while (value >>> 7 !== 0) { valueArray.push((value & 0x7f) | 0x80); value >>>= 7; } valueArray.push(value); this.appendToBuffer(new Uint8Array(valueArray)); } /** * Returns the buffered bytes as a Uint8Array. * * This function allows you to retrieve the byte representation of the buffer up to the current offset. * For better performance, returns a view when the buffer is exactly the right size, or copies * only the used portion otherwise. * * @returns Uint8Array - The byte array representation of the buffer. * @group Implementation * @category BCS */ toUint8Array(): Uint8Array { // Return a copy of only the used portion of the buffer // Using subarray + slice pattern for efficiency return new Uint8Array(this.buffer, 0, this.offset).slice(); } /** * Resets the serializer to its initial state, allowing the buffer to be reused. * This clears the buffer contents to prevent data leakage between uses. * * @group Implementation * @category BCS */ reset(): void { // Clear buffer contents to prevent data leakage when reusing pooled serializers // Only clear the portion that was used (up to offset) for efficiency if (this.offset > 0) { new Uint8Array(this.buffer, 0, this.offset).fill(0); } this.offset = 0; } /** * Returns the current number of bytes written to the serializer. * * @returns The number of bytes written. * @group Implementation * @category BCS */ getOffset(): number { return this.offset; } /** * Returns a view of the serialized bytes without copying. * WARNING: The returned view is only valid until the next write operation. * Use toUint8Array() if you need a persistent copy. * * @returns A Uint8Array view of the buffer (not a copy). * @group Implementation * @category BCS */ toUint8ArrayView(): Uint8Array { return new Uint8Array(this.buffer, 0, this.offset); } /** * Serializes a `Serializable` value, facilitating composable serialization. * * @param value The Serializable value to serialize. * * @returns the serializer instance * @group Implementation * @category BCS */ serialize(value: T): void { // NOTE: The `serialize` method called by `value` is defined in `value`'s // Serializable interface, not the one defined in this class. value.serialize(this); } /** * Serializes a Serializable value as a byte array with a length prefix. * This is the optimized pattern for entry function argument serialization. * * Instead of: * ```typescript * const bcsBytes = value.bcsToBytes(); // Creates new Serializer, copies bytes * serializer.serializeBytes(bcsBytes); * ``` * * Use: * ```typescript * serializer.serializeAsBytes(value); // Uses pooled Serializer, avoids extra copy * ``` * * This method uses a pooled Serializer instance to reduce allocations and * directly appends the serialized bytes with a length prefix. * * @param value - The Serializable value to serialize as bytes. * @group Implementation * @category BCS */ serializeAsBytes(value: T): void { // Acquire a pooled serializer for temporary use const tempSerializer = acquireSerializer(); try { // Serialize the value to the temporary serializer value.serialize(tempSerializer); // Get the serialized bytes (as a view to avoid copying) const bytes = tempSerializer.toUint8ArrayView(); // Serialize with length prefix to this serializer this.serializeBytes(bytes); } finally { // Return the serializer to the pool for reuse releaseSerializer(tempSerializer); } } /** * Serializes an array of BCS Serializable values to a serializer instance. * The bytes are added to the serializer instance's byte buffer. * * @param values The array of BCS Serializable values * @example * const addresses = new Array( * AccountAddress.from("0x1"), * AccountAddress.from("0x2"), * AccountAddress.from("0xa"), * AccountAddress.from("0xb"), * ); * const serializer = new Serializer(); * serializer.serializeVector(addresses); * const serializedBytes = serializer.toUint8Array(); * // serializedBytes is now the BCS-serialized bytes * // The equivalent value in Move would be: * // `bcs::to_bytes(&vector
[@0x1, @0x2, @0xa, @0xb])`; * @group Implementation * @category BCS */ serializeVector(values: Array): void { this.serializeU32AsUleb128(values.length); values.forEach((item) => { item.serialize(this); }); } /** * Serializes an optional value which can be a Serializable, string, or Uint8Array. * For strings and Uint8Arrays, it uses the appropriate serialization method. * * @param value The value to serialize (Serializable, string, Uint8Array, or undefined) * @param len Optional fixed length for Uint8Array serialization. If provided, uses serializeFixedBytes instead of serializeBytes * * @example * ```typescript * const serializer = new Serializer(); * serializer.serializeOption("hello"); // Serializes optional string * serializer.serializeOption(new Uint8Array([1, 2, 3])); // Serializes optional bytes * serializer.serializeOption(new Uint8Array([1, 2, 3]), 3); // Serializes optional fixed-length bytes * serializer.serializeOption(new AccountAddress(...)); // Serializes optional Serializable * serializer.serializeOption(undefined); // Serializes none case * ``` * @group Implementation * @category BCS */ serializeOption(value?: T, len?: number): void { const hasValue = value !== undefined; this.serializeBool(hasValue); if (hasValue) { if (typeof value === "string") { this.serializeStr(value); } else if (value instanceof Uint8Array) { if (len !== undefined) { this.serializeFixedBytes(value); } else { this.serializeBytes(value); } } else { value.serialize(this); } } } /** * @deprecated use `serializeOption` instead. * Serializes an optional string, supporting UTF8 encoding. * The function encodes the existence of the string first, followed by the length and content if it exists. * * BCS layout for optional "string": 1 | string_length | string_content * where string_length is a u32 integer encoded as a uleb128 integer, equal to the number of bytes in string_content. * BCS layout for undefined: 0 * * @param value - The optional string to serialize. If undefined, it will serialize as 0. * @group Implementation * @category BCS */ serializeOptionStr(value?: string): void { if (value === undefined) { this.serializeU32AsUleb128(0); } else { this.serializeU32AsUleb128(1); this.serializeStr(value); } } } /** * @group Implementation * @category BCS */ export function ensureBoolean(value: unknown): asserts value is boolean { if (typeof value !== "boolean") { throw new Error(`${value} is not a boolean value`); } } /** * @group Implementation * @category BCS */ export const outOfRangeErrorMessage = (value: AnyNumber, min: AnyNumber, max: AnyNumber) => `${value} is out of range: [${min}, ${max}]`; /** * Validates that a given number is within a specified range. * This function throws an error if the value is outside the defined minimum and maximum bounds. * * @param value - The number to validate. * @param minValue - The minimum allowable value (inclusive). * @param maxValue - The maximum allowable value (inclusive). * @group Implementation * @category BCS */ export function validateNumberInRange(value: T, minValue: T, maxValue: T) { const valueBigInt = BigInt(value); if (valueBigInt > BigInt(maxValue) || valueBigInt < BigInt(minValue)) { throw new Error(outOfRangeErrorMessage(value, minValue, maxValue)); } } /** * A decorator that validates that the input argument for a function is within a specified range. * This ensures that the function is only called with valid input values, preventing potential errors. * * @param minValue - The input argument must be greater than or equal to this value. * @param maxValue - The input argument must be less than or equal to this value. * @group Implementation * @category BCS */ function checkNumberRange(minValue: T, maxValue: T) { return (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { const childFunction = descriptor.value; descriptor.value = function deco(value: AnyNumber) { validateNumberInRange(value, minValue, maxValue); return childFunction.apply(this, [value]); }; return descriptor; }; }