UNPKG

strictencode

Version:

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

330 lines (297 loc) 11.6 kB
/** * @fileoverview RGB20 Encoder Utilities * * Provides specialized encoders for RGB20 smart contract data structures including * AssetSpec, ContractTerms, and Amount types used in RGB20 token contracts. * * @author RGB Community * @license Apache-2.0 */ import { StrictEncoder } from './index.js'; /** * RGB20 type identifiers used in global state HashMap * @readonly * @enum {number} */ export const RGB20_TYPE_IDS = { /** AssetSpec type ID */ ASSET_SPEC: 2000, /** ContractTerms type ID */ CONTRACT_TERMS: 2001, /** Amount type ID */ AMOUNT: 2002 }; /** * RGB20Encoder provides specialized encoding functions for RGB20 smart contract data structures. * All methods follow the StrictEncode specification and produce deterministic output * suitable for consensus-critical applications. * * @example * const spec = { ticker: "BTC", name: "Bitcoin", precision: 8 }; * const encoded = RGB20Encoder.encodeAssetSpec(spec); * console.log(encoded); // "0342544305426974636f696e0800" */ export class RGB20Encoder { /** * Encode RGB20 AssetSpec structure. * * AssetSpec contains metadata about the RGB20 asset including ticker symbol, * full name, decimal precision, and optional additional details. * * @param {Object} spec - Asset specification * @param {string} spec.ticker - Asset ticker symbol (e.g., "BTC", "NIATCKR") * @param {string} spec.name - Full asset name (e.g., "Bitcoin") * @param {number} spec.precision - Decimal places (0-255) * @param {string|null} [spec.details] - Optional additional details * @returns {string} Hex-encoded StrictEncode bytes * @throws {Error} If input validation fails * * @example * const spec = { * ticker: "NIATCKR", * name: "NIA asset name", * precision: 8, * details: null * }; * const encoded = RGB20Encoder.encodeAssetSpec(spec); * // Returns: "074e494154434b520e4e4941206173736574206e616d650800" */ static encodeAssetSpec(spec) { if (!spec || typeof spec !== 'object') { throw new Error('AssetSpec must be an object'); } const { ticker, name, precision, details } = spec; if (typeof ticker !== 'string' || ticker.length === 0) { throw new Error('ticker must be a non-empty string'); } if (typeof name !== 'string' || name.length === 0) { throw new Error('name must be a non-empty string'); } if (!Number.isInteger(precision) || precision < 0 || precision > 255) { throw new Error('precision must be integer 0-255'); } const encoder = new StrictEncoder(); encoder.encodeString(ticker); encoder.encodeString(name); encoder.encodeU8(precision); encoder.encodeOption(details, (d) => encoder.encodeString(d)); return encoder.toHex(); } /** * Encode RGB20 ContractTerms structure. * * ContractTerms contains the legal/descriptive text of the contract * and optional media references. * * @param {Object} terms - Contract terms * @param {string} terms.text - Contract terms text * @param {string|null} [terms.media] - Optional media reference/URL * @returns {string} Hex-encoded StrictEncode bytes * @throws {Error} If input validation fails * * @example * const terms = { * text: "Standard RGB20 token contract", * media: null * }; * const encoded = RGB20Encoder.encodeContractTerms(terms); */ static encodeContractTerms(terms) { if (!terms || typeof terms !== 'object') { throw new Error('ContractTerms must be an object'); } const { text, media } = terms; if (typeof text !== 'string' || text.length === 0) { throw new Error('text must be a non-empty string'); } const encoder = new StrictEncoder(); encoder.encodeString(text); encoder.encodeOption(media, (m) => encoder.encodeString(m)); return encoder.toHex(); } /** * Encode RGB20 Amount (token quantity). * * Amount represents the number of tokens as a 64-bit unsigned integer. * This is the atomic unit count (e.g., for 1.5 BTC with 8 decimal places, * the amount would be 150000000). * * @param {number|bigint|string} amount - Token amount in atomic units * @returns {string} Hex-encoded StrictEncode bytes (8 bytes, little-endian) * @throws {Error} If amount is invalid * * @example * const amount = 1000000; // 1 million atomic units * const encoded = RGB20Encoder.encodeAmount(amount); * // Returns: "40420f0000000000" (little-endian u64) */ static encodeAmount(amount) { const encoder = new StrictEncoder(); encoder.encodeU64(amount); return encoder.toHex(); } /** * Encode RGB20 global state as HashMap<usize, bytes>. * * The global state contains the core RGB20 contract data indexed by type IDs: * - 2000: AssetSpec * - 2001: ContractTerms * - 2002: Amount * * @param {Object} state - Global state components * @param {Object} state.assetSpec - Asset specification object * @param {Object} state.contractTerms - Contract terms object * @param {number|bigint|string} state.amount - Token amount * @returns {string} Hex-encoded StrictEncode bytes * @throws {Error} If validation fails * * @example * const state = { * assetSpec: { ticker: "RGB", name: "RGB Token", precision: 0 }, * contractTerms: { text: "Test contract", media: null }, * amount: 1000000 * }; * const encoded = RGB20Encoder.encodeGlobalState(state); */ static encodeGlobalState(state) { if (!state || typeof state !== 'object') { throw new Error('Global state must be an object'); } const { assetSpec, contractTerms, amount } = state; if (!assetSpec) throw new Error('assetSpec is required'); if (!contractTerms) throw new Error('contractTerms is required'); if (amount === undefined || amount === null) throw new Error('amount is required'); // Encode each component const specHex = this.encodeAssetSpec(assetSpec); const termsHex = this.encodeContractTerms(contractTerms); const amountHex = this.encodeAmount(amount); // Create HashMap with type IDs const stateMap = { [RGB20_TYPE_IDS.ASSET_SPEC]: specHex, [RGB20_TYPE_IDS.CONTRACT_TERMS]: termsHex, [RGB20_TYPE_IDS.AMOUNT]: amountHex }; const encoder = new StrictEncoder(); encoder.encodeHashMap(stateMap, function(hexString) { // Convert hex string back to bytes for encoding const bytes = new Uint8Array( hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)) ); this._appendBytes(bytes); }); return encoder.toHex(); } /** * Create a complete RGB20 genesis structure for contract ID generation. * * This creates the simplified genesis structure used in educational/playground * implementations. Real RGB contracts use much more complex commitment structures. * * @param {Object} config - Genesis configuration * @param {Object} config.assetSpec - Asset specification * @param {Object} config.contractTerms - Contract terms * @param {number|bigint|string} config.amount - Token amount * @param {string} config.utxo - UTXO reference (txid:vout format) * @param {string} [config.schemaId='rgb20'] - Schema identifier * @returns {Object} Genesis structure suitable for JSON serialization and hashing * * @example * const genesis = RGB20Encoder.createGenesis({ * assetSpec: { ticker: "TEST", name: "Test Token", precision: 8 }, * contractTerms: { text: "Test contract", media: null }, * amount: 1000000, * utxo: "abc123...def:0" * }); * * // Can then be used for contract ID generation: * const serialized = JSON.stringify(genesis, Object.keys(genesis).sort()); * const contractId = sha256(serialized); */ static createGenesis(config) { if (!config || typeof config !== 'object') { throw new Error('Genesis config must be an object'); } const { assetSpec, contractTerms, amount, utxo, schemaId = 'rgb20' } = config; if (!utxo || typeof utxo !== 'string') { throw new Error('utxo must be a string'); } // Validate UTXO format (basic check) if (!utxo.includes(':')) { throw new Error('utxo must be in format "txid:vout"'); } // Encode components const specHex = this.encodeAssetSpec(assetSpec); const termsHex = this.encodeContractTerms(contractTerms); const amountHex = this.encodeAmount(amount); return { schema_id: schemaId, global_state: { [RGB20_TYPE_IDS.ASSET_SPEC]: specHex, [RGB20_TYPE_IDS.CONTRACT_TERMS]: termsHex, [RGB20_TYPE_IDS.AMOUNT]: amountHex }, utxo: utxo.replace(/[^a-f0-9:]/gi, '') // Clean hex chars only }; } /** * Validate an RGB20 AssetSpec structure. * @param {Object} spec - AssetSpec to validate * @returns {boolean} True if valid * @throws {Error} If validation fails */ static validateAssetSpec(spec) { this.encodeAssetSpec(spec); // Will throw if invalid return true; } /** * Validate RGB20 ContractTerms structure. * @param {Object} terms - ContractTerms to validate * @returns {boolean} True if valid * @throws {Error} If validation fails */ static validateContractTerms(terms) { this.encodeContractTerms(terms); // Will throw if invalid return true; } /** * Validate RGB20 Amount value. * @param {number|bigint|string} amount - Amount to validate * @returns {boolean} True if valid * @throws {Error} If validation fails */ static validateAmount(amount) { this.encodeAmount(amount); // Will throw if invalid return true; } } /** * Convenience functions for common RGB20 operations */ /** * Quick encode for AssetSpec * @param {string} ticker - Asset ticker * @param {string} name - Asset name * @param {number} precision - Decimal precision * @param {string|null} details - Optional details * @returns {string} Hex-encoded bytes */ export function encodeAssetSpec(ticker, name, precision, details = null) { return RGB20Encoder.encodeAssetSpec({ ticker, name, precision, details }); } /** * Quick encode for ContractTerms * @param {string} text - Contract terms text * @param {string|null} media - Optional media reference * @returns {string} Hex-encoded bytes */ export function encodeContractTerms(text, media = null) { return RGB20Encoder.encodeContractTerms({ text, media }); } /** * Quick encode for Amount * @param {number|bigint|string} amount - Token amount * @returns {string} Hex-encoded bytes */ export function encodeAmount(amount) { return RGB20Encoder.encodeAmount(amount); }