UNPKG

strictencode

Version:

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

693 lines (583 loc) โ€ข 22.2 kB
#!/usr/bin/env node /** * StrictEncode CLI - Command line interface for deterministic binary encoding * * Provides interactive encoding/decoding capabilities for the StrictEncode library * with extensive examples and RGB20 contract support. * * @author RGB Community * @license Apache-2.0 */ import { StrictEncoder, RGB20Encoder, RGB20_TYPE_IDS } from './index.js'; import { readFileSync } from 'fs'; import { createHash } from 'crypto'; const args = process.argv.slice(2); const command = args[0]; /** * Color codes for terminal output */ const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', gray: '\x1b[90m' }; const c = (color, text) => `${colors[color]}${text}${colors.reset}`; /** * Show help information */ function showHelp() { console.log(` ${c('cyan', '๐Ÿ”ง StrictEncode CLI')} - Deterministic binary encoding for RGB protocol ${c('bright', 'USAGE:')} strictencode <command> [options] [arguments] ${c('bright', 'COMMANDS:')} ${c('green', 'Basic Encoding:')} u8 <value> Encode 8-bit unsigned integer (0-255) u16 <value> Encode 16-bit unsigned integer (0-65535) u32 <value> Encode 32-bit unsigned integer (0-4294967295) u64 <value> Encode 64-bit unsigned integer (0-18446744073709551615) bool <true|false> Encode boolean value string <text> Encode UTF-8 string with LEB128 length leb128 <value> Encode variable-length integer ${c('green', 'Collection Encoding:')} vec <type> <val1,val2> Encode vector of values option <type> <value> Encode optional value (use 'null' for None) ${c('green', 'RGB20 Encoding:')} asset-spec Encode RGB20 AssetSpec structure contract-terms Encode RGB20 ContractTerms structure amount <value> Encode RGB20 token amount genesis Create RGB20 genesis structure ${c('green', 'Utilities:')} combine <hex1> <hex2> Combine hex encodings examples Show comprehensive examples interactive Start interactive mode test-vectors Show specification test vectors ${c('green', 'Help:')} help, -h, --help Show this help message version, -v, --version Show version information ${c('bright', 'EXAMPLES:')} ${c('gray', '# Basic types')} strictencode u32 1000000 strictencode string "Hello RGB" strictencode leb128 200 ${c('gray', '# RGB20 asset specification')} strictencode asset-spec --ticker BTC --name Bitcoin --precision 8 ${c('gray', '# Interactive exploration')} strictencode interactive strictencode examples ${c('bright', 'OPTIONS:')} --hex Output as hex string (default) --bytes Output as byte array --json Output as JSON object --verbose Show encoding breakdown --no-color Disable colored output For more examples: ${c('blue', 'strictencode examples')} Interactive mode: ${c('blue', 'strictencode interactive')} `); } /** * Show version information */ function showVersion() { const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); console.log(`strictencode v${pkg.version}`); console.log('Deterministic binary encoding for RGB protocol compliance'); } /** * Parse command line options */ function parseOptions(args) { const options = { format: 'hex', verbose: false, color: true }; const filtered = args.filter(arg => { if (arg === '--hex') { options.format = 'hex'; return false; } if (arg === '--bytes') { options.format = 'bytes'; return false; } if (arg === '--json') { options.format = 'json'; return false; } if (arg === '--verbose') { options.verbose = true; return false; } if (arg === '--no-color') { options.color = false; return false; } return true; }); return { options, args: filtered }; } /** * Format output based on options */ function formatOutput(encoder, options, description = '') { const hex = encoder.toHex(); const bytes = encoder.toBytes(); if (!options.color) { Object.keys(colors).forEach(key => colors[key] = ''); } if (options.verbose && description) { console.log(c('yellow', `๐Ÿ“ ${description}`)); } switch (options.format) { case 'bytes': console.log(Array.from(bytes).join(' ')); break; case 'json': console.log(JSON.stringify({ hex: hex, bytes: Array.from(bytes), length: bytes.length, description: description }, null, 2)); break; default: // hex console.log(hex); break; } if (options.verbose) { console.log(c('gray', `Length: ${bytes.length} bytes`)); if (hex.length <= 32) { console.log(c('gray', `Bytes: [${Array.from(bytes).join(', ')}]`)); } } } /** * Show comprehensive examples */ function showExamples() { console.log(` ${c('cyan', '๐Ÿ“š StrictEncode Examples')} ${c('bright', '๐Ÿ”ข Basic Integer Encoding:')} strictencode u8 255 # ${c('green', 'ff')} strictencode u16 65535 # ${c('green', 'ffff')} (little-endian) strictencode u32 1000000 # ${c('green', '40420f00')} (little-endian) strictencode u64 1000000 # ${c('green', '40420f0000000000')} (little-endian) ${c('bright', 'โœ… Boolean Encoding:')} strictencode bool true # ${c('green', '01')} strictencode bool false # ${c('green', '00')} ${c('bright', '๐Ÿ”ค String Encoding:')} strictencode string "RGB" # ${c('green', '03524742')} (length + UTF-8) strictencode string "Hello" # ${c('green', '0548656c6c6f')} strictencode string "" # ${c('green', '00')} (empty string) ${c('bright', '๐Ÿ“Š LEB128 Variable-Length Encoding:')} strictencode leb128 7 # ${c('green', '07')} (single byte) strictencode leb128 127 # ${c('green', '7f')} (max single byte) strictencode leb128 128 # ${c('green', '8001')} (multi-byte) strictencode leb128 200 # ${c('green', 'c801')} (multi-byte) ${c('bright', '๐Ÿ“ฆ Collection Encoding:')} strictencode vec u8 1,2,3 # ${c('green', '03010203')} (length + items) strictencode option string "test" # ${c('green', '010474657374')} (Some) strictencode option string null # ${c('green', '00')} (None) ${c('bright', '๐ŸŒˆ RGB20 Asset Specification:')} strictencode asset-spec --ticker NIATCKR --name "NIA asset name" --precision 8 # ${c('green', '074e494154434b520e4e4941206173736574206e616d650800')} ${c('bright', '๐Ÿ“„ RGB20 Contract Terms:')} strictencode contract-terms --text "NIA terms" # ${c('green', '094e4941207465726d7300')} ${c('bright', '๐Ÿ’ฐ RGB20 Token Amount:')} strictencode amount 1000000 # ${c('green', '40420f0000000000')} (u64 little-endian) ${c('bright', '๐Ÿ—๏ธ RGB20 Complete Genesis:')} strictencode genesis \\ --ticker TESTCOIN \\ --name "Test Coin" \\ --precision 8 \\ --terms "Test contract" \\ --amount 21000000 \\ --utxo "abc123...def:0" ${c('bright', '๐Ÿ”ง Utility Commands:')} strictencode combine 03524742 0548656c6c6f # Combine hex strings strictencode test-vectors # Show specification test cases strictencode interactive # Interactive exploration mode ${c('bright', 'โšก Advanced Options:')} strictencode u32 1000000 --verbose # Show encoding breakdown strictencode string "test" --bytes # Output as byte array strictencode amount 1000000 --json # Output as JSON strictencode examples --no-color # Disable colors ${c('bright', '๐ŸŽฏ Real-World RGB20 Example:')} ${c('gray', '# Create a complete RGB20 token')} strictencode genesis \\ --ticker BTC \\ --name "Bitcoin" \\ --precision 8 \\ --terms "Peer-to-peer electronic cash system" \\ --amount 2100000000000000 \\ --utxo "6a12c58f92d73cd8a685c55b3f0e7d5e2b4a1c23456789abcdef0123456789ab:0" ${c('bright', '๐Ÿงช Specification Test Vectors:')} strictencode test-vectors # Shows all test cases from specification Try: ${c('blue', 'strictencode interactive')} for hands-on exploration! `); } /** * Show specification test vectors */ function showTestVectors() { console.log(` ${c('cyan', '๐Ÿงช StrictEncode Specification Test Vectors')} ${c('bright', 'From STRICT-ENCODE-SPECIFICATION.md:')} ${c('yellow', 'Basic Types:')} u8(255) โ†’ ff u16(65535) โ†’ ffff u32(1000000) โ†’ 40420f00 u64(1000000) โ†’ 40420f0000000000 bool(true) โ†’ 01 bool(false) โ†’ 00 ${c('yellow', 'LEB128 Encoding:')} leb128(7) โ†’ 07 leb128(127) โ†’ 7f leb128(128) โ†’ 8001 leb128(200) โ†’ c801 ${c('yellow', 'String Encoding:')} "RGB" โ†’ 03524742 "NIATCKR" โ†’ 074e494154434b52 "" โ†’ 00 ${c('yellow', 'RGB20 Test Case:')} AssetSpec { ticker: "NIATCKR", name: "NIA asset name", precision: 8, details: null } โ†’ 074e494154434b520e4e4941206173736574206e616d650800 ContractTerms { text: "NIA terms", media: null } โ†’ 094e4941207465726d7300 Amount(1000000) โ†’ 40420f0000000000 ${c('bright', 'Validate any test vector:')} strictencode string "NIATCKR" # Should output: 074e494154434b52 strictencode leb128 200 # Should output: c801 strictencode u32 1000000 # Should output: 40420f00 `); } /** * Interactive mode */ async function interactiveMode() { console.log(` ${c('cyan', '๐ŸŽฎ StrictEncode Interactive Mode')} Type commands to explore encoding. Examples: โ€ข ${c('green', 'u32 1000000')} โ€ข ${c('green', 'string "Hello RGB"')} โ€ข ${c('green', 'asset-spec --ticker BTC --name Bitcoin --precision 8')} โ€ข ${c('green', 'examples')} - show all examples โ€ข ${c('green', 'help')} - show help โ€ข ${c('green', 'exit')} - quit `); const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: c('blue', 'strictencode> ') }); rl.prompt(); rl.on('line', (line) => { const input = line.trim(); if (!input) { rl.prompt(); return; } if (input === 'exit' || input === 'quit') { console.log(c('yellow', 'Goodbye! ๐Ÿ‘‹')); rl.close(); return; } try { const args = input.split(' ').filter(Boolean); processCommand(args[0], args.slice(1)); } catch (error) { console.log(c('red', `Error: ${error.message}`)); } console.log(); rl.prompt(); }); rl.on('close', () => { process.exit(0); }); } /** * Process individual commands */ function processCommand(cmd, args) { const { options, args: filteredArgs } = parseOptions(args); const encoder = new StrictEncoder(); let description = ''; try { switch (cmd) { case 'u8': if (!filteredArgs[0]) throw new Error('u8 requires a value (0-255)'); const u8Val = parseInt(filteredArgs[0]); encoder.encodeU8(u8Val); description = `u8(${u8Val})`; break; case 'u16': if (!filteredArgs[0]) throw new Error('u16 requires a value (0-65535)'); const u16Val = parseInt(filteredArgs[0]); encoder.encodeU16(u16Val); description = `u16(${u16Val}) little-endian`; break; case 'u32': if (!filteredArgs[0]) throw new Error('u32 requires a value (0-4294967295)'); const u32Val = parseInt(filteredArgs[0]); encoder.encodeU32(u32Val); description = `u32(${u32Val}) little-endian`; break; case 'u64': if (!filteredArgs[0]) throw new Error('u64 requires a value'); const u64Val = filteredArgs[0]; encoder.encodeU64(u64Val); description = `u64(${u64Val}) little-endian`; break; case 'bool': if (!filteredArgs[0]) throw new Error('bool requires true or false'); const boolVal = filteredArgs[0] === 'true'; encoder.encodeBool(boolVal); description = `bool(${boolVal})`; break; case 'string': if (!filteredArgs[0]) throw new Error('string requires text'); const strVal = filteredArgs.join(' '); encoder.encodeString(strVal); description = `string("${strVal}") with LEB128 length`; break; case 'leb128': if (!filteredArgs[0]) throw new Error('leb128 requires a value'); const lebVal = parseInt(filteredArgs[0]); encoder.encodeLeb128(lebVal); description = `leb128(${lebVal}) variable-length`; break; case 'vec': if (filteredArgs.length < 2) throw new Error('vec requires type and values'); const vecType = filteredArgs[0]; const vecValues = filteredArgs[1].split(','); encoder.encodeVec(vecValues, function(item) { switch (vecType) { case 'u8': this.encodeU8(parseInt(item)); break; case 'string': this.encodeString(item); break; default: throw new Error(`Unsupported vec type: ${vecType}`); } }); description = `Vec<${vecType}>[${vecValues.join(',')}]`; break; case 'option': if (filteredArgs.length < 2) throw new Error('option requires type and value'); const optType = filteredArgs[0]; const optValue = filteredArgs[1] === 'null' ? null : filteredArgs.slice(1).join(' '); encoder.encodeOption(optValue, function(value) { switch (optType) { case 'string': this.encodeString(value); break; case 'u32': this.encodeU32(parseInt(value)); break; default: throw new Error(`Unsupported option type: ${optType}`); } }); description = `Option<${optType}>(${optValue === null ? 'None' : optValue})`; break; case 'asset-spec': return processAssetSpec(filteredArgs, options); case 'contract-terms': return processContractTerms(filteredArgs, options); case 'amount': if (!filteredArgs[0]) throw new Error('amount requires a value'); const amountHex = RGB20Encoder.encodeAmount(filteredArgs[0]); console.log(amountHex); if (options.verbose) { console.log(c('gray', `RGB20 Amount(${filteredArgs[0]}) as u64 little-endian`)); } return; case 'genesis': return processGenesis(filteredArgs, options); case 'combine': if (filteredArgs.length < 2) throw new Error('combine requires at least 2 hex strings'); const combined = filteredArgs.join(''); console.log(combined); if (options.verbose) { console.log(c('gray', `Combined ${filteredArgs.length} hex strings`)); } return; case 'examples': showExamples(); return; case 'test-vectors': showTestVectors(); return; case 'interactive': interactiveMode(); return; case 'help': case '-h': case '--help': showHelp(); return; case 'version': case '-v': case '--version': showVersion(); return; default: throw new Error(`Unknown command: ${cmd}. Use 'help' for available commands.`); } formatOutput(encoder, options, description); } catch (error) { console.log(c('red', `Error: ${error.message}`)); if (error.message.includes('requires')) { console.log(c('gray', `Try: strictencode help`)); } } } /** * Process asset-spec command */ function processAssetSpec(args, options) { const spec = { ticker: null, name: null, precision: 8, details: null }; for (let i = 0; i < args.length; i++) { if (args[i] === '--ticker' && args[i + 1]) { spec.ticker = args[i + 1]; i++; } else if (args[i] === '--name' && args[i + 1]) { spec.name = args[i + 1]; i++; } else if (args[i] === '--precision' && args[i + 1]) { spec.precision = parseInt(args[i + 1]); i++; } else if (args[i] === '--details' && args[i + 1]) { spec.details = args[i + 1]; i++; } } if (!spec.ticker) spec.ticker = 'DEMO'; if (!spec.name) spec.name = 'Demo Token'; const encoded = RGB20Encoder.encodeAssetSpec(spec); console.log(encoded); if (options.verbose) { console.log(c('yellow', '๐Ÿ“ RGB20 AssetSpec breakdown:')); console.log(c('gray', ` Ticker: "${spec.ticker}"`)); console.log(c('gray', ` Name: "${spec.name}"`)); console.log(c('gray', ` Precision: ${spec.precision}`)); console.log(c('gray', ` Details: ${spec.details || 'None'}`)); } } /** * Process contract-terms command */ function processContractTerms(args, options) { const terms = { text: null, media: null }; for (let i = 0; i < args.length; i++) { if (args[i] === '--text' && args[i + 1]) { terms.text = args[i + 1]; i++; } else if (args[i] === '--media' && args[i + 1]) { terms.media = args[i + 1]; i++; } } if (!terms.text) terms.text = 'Demo contract terms'; const encoded = RGB20Encoder.encodeContractTerms(terms); console.log(encoded); if (options.verbose) { console.log(c('yellow', '๐Ÿ“ RGB20 ContractTerms breakdown:')); console.log(c('gray', ` Text: "${terms.text}"`)); console.log(c('gray', ` Media: ${terms.media || 'None'}`)); } } /** * Process genesis command */ function processGenesis(args, options) { const config = { ticker: 'DEMO', name: 'Demo Token', precision: 8, terms: 'Demo contract terms', amount: 1000000, utxo: 'abc123def456789abc123def456789abc123def456789abc123def456789:0' }; for (let i = 0; i < args.length; i++) { if (args[i] === '--ticker' && args[i + 1]) { config.ticker = args[i + 1]; i++; } else if (args[i] === '--name' && args[i + 1]) { config.name = args[i + 1]; i++; } else if (args[i] === '--precision' && args[i + 1]) { config.precision = parseInt(args[i + 1]); i++; } else if (args[i] === '--terms' && args[i + 1]) { config.terms = args[i + 1]; i++; } else if (args[i] === '--amount' && args[i + 1]) { config.amount = args[i + 1]; i++; } else if (args[i] === '--utxo' && args[i + 1]) { config.utxo = args[i + 1]; i++; } } const genesis = RGB20Encoder.createGenesis({ assetSpec: { ticker: config.ticker, name: config.name, precision: config.precision, details: null }, contractTerms: { text: config.terms, media: null }, amount: config.amount, utxo: config.utxo }); if (options.format === 'json') { console.log(JSON.stringify(genesis, null, 2)); } else { // Create contract ID hash const serialized = JSON.stringify(genesis, Object.keys(genesis).sort()); const contractId = createHash('sha256').update(serialized).digest('hex'); console.log(c('bright', '๐Ÿ—๏ธ RGB20 Genesis Structure:')); console.log(JSON.stringify(genesis, null, 2)); console.log(); console.log(c('bright', '๐Ÿ”‘ Contract ID (SHA-256):')); console.log(contractId); if (options.verbose) { console.log(); console.log(c('yellow', '๐Ÿ“ Components:')); console.log(c('gray', ` AssetSpec: ${genesis.global_state[RGB20_TYPE_IDS.ASSET_SPEC]}`)); console.log(c('gray', ` ContractTerms: ${genesis.global_state[RGB20_TYPE_IDS.CONTRACT_TERMS]}`)); console.log(c('gray', ` Amount: ${genesis.global_state[RGB20_TYPE_IDS.AMOUNT]}`)); } } } /** * Main entry point */ function main() { if (!command || command === 'help' || command === '-h' || command === '--help') { showHelp(); return; } if (command === 'version' || command === '-v' || command === '--version') { showVersion(); return; } processCommand(command, args.slice(1)); } // Handle uncaught errors gracefully process.on('uncaughtException', (error) => { console.error(c('red', `Fatal error: ${error.message}`)); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error(c('red', `Unhandled rejection: ${reason}`)); process.exit(1); }); main();