UNPKG

strictencode

Version:

Deterministic binary encoding for RGB protocol compliance - JavaScript implementation of StrictEncode

333 lines (308 loc) 11.1 kB
/** * @fileoverview StrictEncode - Deterministic binary encoding for RGB protocol compliance * * This module provides a JavaScript implementation of StrictEncode, a deterministic binary * serialization format designed for consensus-critical applications like RGB smart contracts. * * Based on the StrictEncode specification developed by LNP/BP Standards Association. * * @author RGB Community * @license Apache-2.0 * @see https://github.com/rgbjs/strictencode */ /** * StrictEncoder provides deterministic binary encoding for primitive types and collections. * All encoding follows the StrictEncode specification with little-endian byte order and * LEB128 variable-length integer encoding. * * @example * const encoder = new StrictEncoder(); * encoder.encodeU32(1000000).encodeString("RGB20"); * console.log(encoder.toHex()); // "40420f000552474232" */ export class StrictEncoder { /** * Create a new StrictEncoder instance. */ constructor() { /** @type {Uint8Array} */ this.buffer = new Uint8Array(0); } /** * Append bytes to the internal buffer. * @private * @param {Uint8Array} bytes - Bytes to append */ _appendBytes(bytes) { const newBuffer = new Uint8Array(this.buffer.length + bytes.length); newBuffer.set(this.buffer); newBuffer.set(bytes, this.buffer.length); this.buffer = newBuffer; } /** * Encode an unsigned 8-bit integer. * @param {number} value - Integer value (0-255) * @returns {StrictEncoder} This encoder for chaining * @throws {Error} If value is invalid * * @example * encoder.encodeU8(255); // Encodes to: ff */ encodeU8(value) { if (value < 0 || value > 255 || !Number.isInteger(value)) { throw new Error(`Invalid u8 value: ${value}. Must be integer 0-255`); } this._appendBytes(new Uint8Array([value])); return this; } /** * Encode an unsigned 16-bit integer in little-endian format. * @param {number} value - Integer value (0-65535) * @returns {StrictEncoder} This encoder for chaining * @throws {Error} If value is invalid * * @example * encoder.encodeU16(65535); // Encodes to: ffff */ encodeU16(value) { if (value < 0 || value > 65535 || !Number.isInteger(value)) { throw new Error(`Invalid u16 value: ${value}. Must be integer 0-65535`); } const buffer = new ArrayBuffer(2); const view = new DataView(buffer); view.setUint16(0, value, true); // little-endian this._appendBytes(new Uint8Array(buffer)); return this; } /** * Encode an unsigned 32-bit integer in little-endian format. * @param {number} value - Integer value (0-4294967295) * @returns {StrictEncoder} This encoder for chaining * @throws {Error} If value is invalid * * @example * encoder.encodeU32(1000000); // Encodes to: 40420f00 */ encodeU32(value) { if (value < 0 || value > 4294967295 || !Number.isInteger(value)) { throw new Error(`Invalid u32 value: ${value}. Must be integer 0-4294967295`); } const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint32(0, value, true); // little-endian this._appendBytes(new Uint8Array(buffer)); return this; } /** * Encode an unsigned 64-bit integer in little-endian format. * @param {number|bigint|string} value - Integer value (0-18446744073709551615) * @returns {StrictEncoder} This encoder for chaining * @throws {Error} If value is invalid * * @example * encoder.encodeU64(1000000n); // Encodes to: 40420f0000000000 */ encodeU64(value) { const bigIntValue = BigInt(value); if (bigIntValue < 0n || bigIntValue > 18446744073709551615n) { throw new Error(`Invalid u64 value: ${value}. Must be 0-18446744073709551615`); } const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, bigIntValue, true); // little-endian this._appendBytes(new Uint8Array(buffer)); return this; } /** * Encode a boolean value (0x00 for false, 0x01 for true). * @param {boolean} value - Boolean value * @returns {StrictEncoder} This encoder for chaining * * @example * encoder.encodeBool(true); // Encodes to: 01 * encoder.encodeBool(false); // Encodes to: 00 */ encodeBool(value) { this._appendBytes(new Uint8Array([value ? 0x01 : 0x00])); return this; } /** * Encode a usize value using LEB128 variable-length encoding. * LEB128 uses 7 bits per byte for data and 1 bit for continuation. * * @param {number} value - Non-negative integer * @returns {StrictEncoder} This encoder for chaining * @throws {Error} If value is invalid * * @example * encoder.encodeLeb128(7); // Encodes to: 07 * encoder.encodeLeb128(128); // Encodes to: 8001 * encoder.encodeLeb128(200); // Encodes to: c801 */ encodeLeb128(value) { if (value < 0 || !Number.isInteger(value)) { throw new Error(`Invalid LEB128 value: ${value}. Must be non-negative integer`); } const result = []; do { let byte = value & 0x7F; value >>>= 7; if (value !== 0) { byte |= 0x80; } result.push(byte); } while (value !== 0); this._appendBytes(new Uint8Array(result)); return this; } /** * Encode a string using LEB128 length prefix + UTF-8 bytes. * @param {string} str - String to encode * @returns {StrictEncoder} This encoder for chaining * @throws {Error} If string contains invalid UTF-8 * * @example * encoder.encodeString("RGB"); // Encodes to: 03524742 * encoder.encodeString("NIATCKR"); // Encodes to: 074e494154434b52 */ encodeString(str) { if (typeof str !== 'string') { throw new Error(`Invalid string: ${typeof str}. Must be string`); } try { const utf8Bytes = new TextEncoder().encode(str); this.encodeLeb128(utf8Bytes.length); this._appendBytes(utf8Bytes); return this; } catch (error) { throw new Error(`Invalid UTF-8 string: ${str}. ${error.message}`); } } /** * Encode an Option<T> (nullable value). * Uses tag 0x00 for None, 0x01 + encoded value for Some. * * @param {*} value - Value to encode (null/undefined for None) * @param {Function} encoderFunc - Function to encode the inner value if Some * @returns {StrictEncoder} This encoder for chaining * * @example * encoder.encodeOption(null, () => {}); // Encodes to: 00 * encoder.encodeOption("test", (v) => encoder.encodeString(v)); // Encodes to: 010474657374 */ encodeOption(value, encoderFunc) { if (value === null || value === undefined) { this.encodeU8(0x00); // None } else { this.encodeU8(0x01); // Some if (typeof encoderFunc === 'function') { encoderFunc.call(this, value); } else { throw new Error('encoderFunc must be a function for Some values'); } } return this; } /** * Encode a Vec<T> (vector/array) using LEB128 length + encoded items. * @param {Array} array - Array to encode * @param {Function} encoderFunc - Function to encode each item * @returns {StrictEncoder} This encoder for chaining * * @example * encoder.encodeVec(["RGB", "20"], (item) => encoder.encodeString(item)); */ encodeVec(array, encoderFunc) { if (!Array.isArray(array)) { throw new Error(`Invalid Vec: ${typeof array}. Must be array`); } this.encodeLeb128(array.length); array.forEach(item => { if (typeof encoderFunc === 'function') { encoderFunc.call(this, item); } else { throw new Error('encoderFunc must be a function'); } }); return this; } /** * Encode a HashMap<usize, T> as sorted Vec<T>. * Keys are sorted numerically, then values are encoded as Vec<T>. * * @param {Map|Object} map - Map or object to encode * @param {Function} encoderFunc - Function to encode each value * @returns {StrictEncoder} This encoder for chaining * * @example * const stateMap = { 2000: "spec", 2001: "terms", 2002: "amount" }; * encoder.encodeHashMap(stateMap, (value) => encoder.encodeString(value)); */ encodeHashMap(map, encoderFunc) { let entries; if (map instanceof Map) { entries = Array.from(map.entries()); } else if (typeof map === 'object' && map !== null) { entries = Object.entries(map).map(([k, v]) => [parseInt(k), v]); } else { throw new Error(`Invalid HashMap: ${typeof map}. Must be Map or Object`); } // Validate keys are usize (non-negative integers) entries.forEach(([k, v]) => { if (!Number.isInteger(k) || k < 0) { throw new Error(`Invalid HashMap key: ${k}. Keys must be non-negative integers`); } }); // Sort by key (usize) entries.sort((a, b) => a[0] - b[0]); // Encode as Vec of values const values = entries.map(([k, v]) => v); this.encodeVec(values, encoderFunc); return this; } /** * Get the encoded data as a hex string. * @returns {string} Hexadecimal representation of encoded data * * @example * encoder.encodeU8(255).toHex(); // "ff" */ toHex() { return Array.from(this.buffer) .map(b => b.toString(16).padStart(2, '0')) .join(''); } /** * Get the encoded data as a Uint8Array. * @returns {Uint8Array} The encoded bytes */ toBytes() { return this.buffer.slice(); // Return copy } /** * Get the length of encoded data in bytes. * @returns {number} Number of bytes */ length() { return this.buffer.length; } /** * Reset the encoder to empty state. * @returns {StrictEncoder} This encoder for chaining */ reset() { this.buffer = new Uint8Array(0); return this; } /** * Create a copy of this encoder. * @returns {StrictEncoder} New encoder with same data */ clone() { const newEncoder = new StrictEncoder(); newEncoder.buffer = this.buffer.slice(); return newEncoder; } } // Re-export RGB20 utilities export { RGB20Encoder, RGB20_TYPE_IDS, encodeAssetSpec, encodeContractTerms, encodeAmount } from './rgb20.js';