strictencode
Version:
Deterministic binary encoding for RGB protocol compliance - JavaScript implementation of StrictEncode
330 lines (297 loc) • 11.6 kB
JavaScript
/**
* @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);
}