UNPKG

strictencode

Version:

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

422 lines (351 loc) 15.9 kB
/** * @fileoverview Tests for RGB20 encoder utilities */ import { RGB20Encoder, RGB20_TYPE_IDS, encodeAssetSpec, encodeContractTerms, encodeAmount } from '../rgb20.js'; describe('RGB20Encoder', () => { describe('RGB20_TYPE_IDS', () => { test('has correct type identifiers', () => { expect(RGB20_TYPE_IDS.ASSET_SPEC).toBe(2000); expect(RGB20_TYPE_IDS.CONTRACT_TERMS).toBe(2001); expect(RGB20_TYPE_IDS.AMOUNT).toBe(2002); }); }); describe('encodeAssetSpec', () => { test('encodes standard RGB20 test vector', () => { const spec = { ticker: 'NIATCKR', name: 'NIA asset name', precision: 8, details: null }; const encoded = RGB20Encoder.encodeAssetSpec(spec); expect(encoded).toBe('074e494154434b520e4e4941206173736574206e616d650800'); }); test('encodes with details', () => { const spec = { ticker: 'BTC', name: 'Bitcoin', precision: 8, details: 'The first cryptocurrency' }; const encoded = RGB20Encoder.encodeAssetSpec(spec); expect(encoded.length).toBeGreaterThan(0); expect(encoded).toMatch(/^[0-9a-f]+$/); // Valid hex }); test('encodes minimal example', () => { const spec = { ticker: 'T', name: 'Test', precision: 0 }; const encoded = RGB20Encoder.encodeAssetSpec(spec); // Should be: 01T + 04Test + 00precision + 00None expect(encoded).toBe('015404546573740000'); }); test('validates required fields', () => { expect(() => RGB20Encoder.encodeAssetSpec(null)).toThrow('AssetSpec must be an object'); expect(() => RGB20Encoder.encodeAssetSpec({})).toThrow('ticker must be a non-empty string'); expect(() => RGB20Encoder.encodeAssetSpec({ ticker: 'BTC' })).toThrow('name must be a non-empty string'); expect(() => RGB20Encoder.encodeAssetSpec({ ticker: 'BTC', name: 'Bitcoin' })).toThrow('precision must be integer 0-255'); }); test('validates field types', () => { expect(() => RGB20Encoder.encodeAssetSpec({ ticker: '', name: 'Bitcoin', precision: 8 })).toThrow('ticker must be a non-empty string'); expect(() => RGB20Encoder.encodeAssetSpec({ ticker: 123, name: 'Bitcoin', precision: 8 })).toThrow('ticker must be a non-empty string'); expect(() => RGB20Encoder.encodeAssetSpec({ ticker: 'BTC', name: '', precision: 8 })).toThrow('name must be a non-empty string'); expect(() => RGB20Encoder.encodeAssetSpec({ ticker: 'BTC', name: 'Bitcoin', precision: -1 })).toThrow('precision must be integer 0-255'); expect(() => RGB20Encoder.encodeAssetSpec({ ticker: 'BTC', name: 'Bitcoin', precision: 256 })).toThrow('precision must be integer 0-255'); }); }); describe('encodeContractTerms', () => { test('encodes standard RGB20 test vector', () => { const terms = { text: 'NIA terms', media: null }; const encoded = RGB20Encoder.encodeContractTerms(terms); expect(encoded).toBe('094e4941207465726d7300'); }); test('encodes with media', () => { const terms = { text: 'Standard RGB20 contract', media: 'https://example.com/contract.pdf' }; const encoded = RGB20Encoder.encodeContractTerms(terms); expect(encoded.length).toBeGreaterThan(0); expect(encoded).toMatch(/^[0-9a-f]+$/); }); test('validates required fields', () => { expect(() => RGB20Encoder.encodeContractTerms(null)).toThrow('ContractTerms must be an object'); expect(() => RGB20Encoder.encodeContractTerms({})).toThrow('text must be a non-empty string'); expect(() => RGB20Encoder.encodeContractTerms({ text: '' })).toThrow('text must be a non-empty string'); }); }); describe('encodeAmount', () => { test('encodes standard RGB20 test vector', () => { const encoded = RGB20Encoder.encodeAmount(1000000); expect(encoded).toBe('40420f0000000000'); }); test('encodes zero amount', () => { const encoded = RGB20Encoder.encodeAmount(0); expect(encoded).toBe('0000000000000000'); }); test('encodes large amounts', () => { const maxSupply = '21000000000000000'; // 21M BTC in satoshis const encoded = RGB20Encoder.encodeAmount(maxSupply); expect(encoded.length).toBe(16); // 8 bytes = 16 hex chars }); test('handles different input types', () => { const amount = 1000000; expect(RGB20Encoder.encodeAmount(amount)).toBe('40420f0000000000'); expect(RGB20Encoder.encodeAmount(BigInt(amount))).toBe('40420f0000000000'); expect(RGB20Encoder.encodeAmount(amount.toString())).toBe('40420f0000000000'); }); test('validates input', () => { expect(() => RGB20Encoder.encodeAmount(-1)).toThrow('Invalid u64 value'); expect(() => RGB20Encoder.encodeAmount('invalid')).toThrow(); }); }); describe('encodeGlobalState', () => { test('encodes complete global state', () => { const state = { assetSpec: { ticker: 'TEST', name: 'Test Token', precision: 8, details: null }, contractTerms: { text: 'Test contract terms', media: null }, amount: 1000000 }; const encoded = RGB20Encoder.encodeGlobalState(state); expect(encoded.length).toBeGreaterThan(0); expect(encoded).toMatch(/^[0-9a-f]+$/); // Should start with Vec length (3 items) expect(encoded.startsWith('03')).toBe(true); }); test('validates required components', () => { expect(() => RGB20Encoder.encodeGlobalState(null)).toThrow('Global state must be an object'); expect(() => RGB20Encoder.encodeGlobalState({})).toThrow('assetSpec is required'); expect(() => RGB20Encoder.encodeGlobalState({ assetSpec: { ticker: 'T', name: 'Test', precision: 0 } })).toThrow('contractTerms is required'); expect(() => RGB20Encoder.encodeGlobalState({ assetSpec: { ticker: 'T', name: 'Test', precision: 0 }, contractTerms: { text: 'Terms' } })).toThrow('amount is required'); }); }); describe('createGenesis', () => { test('creates complete genesis structure', () => { const config = { assetSpec: { ticker: 'NIATCKR', name: 'NIA asset name', precision: 8, details: null }, contractTerms: { text: 'NIA terms', media: null }, amount: 1000000, utxo: '6a12c58f92d73cd8a685c55b3f0e7d5e2b4a1c23456789abcdef0123456789ab:0' }; const genesis = RGB20Encoder.createGenesis(config); expect(genesis).toHaveProperty('schema_id', 'rgb20'); expect(genesis).toHaveProperty('global_state'); expect(genesis).toHaveProperty('utxo'); expect(genesis.global_state).toHaveProperty('2000'); // AssetSpec expect(genesis.global_state).toHaveProperty('2001'); // ContractTerms expect(genesis.global_state).toHaveProperty('2002'); // Amount // Validate hex encodings expect(genesis.global_state['2000']).toBe('074e494154434b520e4e4941206173736574206e616d650800'); expect(genesis.global_state['2001']).toBe('094e4941207465726d7300'); expect(genesis.global_state['2002']).toBe('40420f0000000000'); }); test('uses custom schema ID', () => { const config = { assetSpec: { ticker: 'T', name: 'Test', precision: 0 }, contractTerms: { text: 'Terms' }, amount: 0, utxo: 'abc123:0', schemaId: 'custom-schema' }; const genesis = RGB20Encoder.createGenesis(config); expect(genesis.schema_id).toBe('custom-schema'); }); test('validates UTXO format', () => { const config = { assetSpec: { ticker: 'T', name: 'Test', precision: 0 }, contractTerms: { text: 'Terms' }, amount: 0, utxo: 'invalid-format' }; expect(() => RGB20Encoder.createGenesis(config)).toThrow('utxo must be in format "txid:vout"'); }); test('cleans UTXO hex characters', () => { const config = { assetSpec: { ticker: 'T', name: 'Test', precision: 0 }, contractTerms: { text: 'Terms' }, amount: 0, utxo: 'ABC-123_def:0' }; const genesis = RGB20Encoder.createGenesis(config); expect(genesis.utxo).toBe('ABC123def:0'); }); }); describe('validation methods', () => { test('validateAssetSpec', () => { const validSpec = { ticker: 'BTC', name: 'Bitcoin', precision: 8 }; expect(RGB20Encoder.validateAssetSpec(validSpec)).toBe(true); expect(() => RGB20Encoder.validateAssetSpec({})).toThrow(); }); test('validateContractTerms', () => { const validTerms = { text: 'Contract terms' }; expect(RGB20Encoder.validateContractTerms(validTerms)).toBe(true); expect(() => RGB20Encoder.validateContractTerms({})).toThrow(); }); test('validateAmount', () => { expect(RGB20Encoder.validateAmount(1000000)).toBe(true); expect(RGB20Encoder.validateAmount(0)).toBe(true); expect(() => RGB20Encoder.validateAmount(-1)).toThrow(); }); }); describe('convenience functions', () => { test('encodeAssetSpec function', () => { const result = encodeAssetSpec('BTC', 'Bitcoin', 8); expect(result).toBe(RGB20Encoder.encodeAssetSpec({ ticker: 'BTC', name: 'Bitcoin', precision: 8, details: null })); }); test('encodeAssetSpec with details', () => { const result = encodeAssetSpec('BTC', 'Bitcoin', 8, 'Details'); expect(result).toBe(RGB20Encoder.encodeAssetSpec({ ticker: 'BTC', name: 'Bitcoin', precision: 8, details: 'Details' })); }); test('encodeContractTerms function', () => { const result = encodeContractTerms('Terms'); expect(result).toBe(RGB20Encoder.encodeContractTerms({ text: 'Terms', media: null })); }); test('encodeContractTerms with media', () => { const result = encodeContractTerms('Terms', 'Media'); expect(result).toBe(RGB20Encoder.encodeContractTerms({ text: 'Terms', media: 'Media' })); }); test('encodeAmount function', () => { const result = encodeAmount(1000000); expect(result).toBe(RGB20Encoder.encodeAmount(1000000)); }); }); describe('integration tests', () => { test('complete RGB20 workflow', () => { // Create asset specification const assetSpec = { ticker: 'TESTCOIN', name: 'Test Coin', precision: 8, details: 'A test cryptocurrency' }; // Create contract terms const contractTerms = { text: 'This is a test RGB20 contract for demonstration purposes.', media: 'https://example.com/logo.png' }; // Set initial supply const amount = 21000000 * Math.pow(10, assetSpec.precision); // 21M coins // Encode each component const specHex = RGB20Encoder.encodeAssetSpec(assetSpec); const termsHex = RGB20Encoder.encodeContractTerms(contractTerms); const amountHex = RGB20Encoder.encodeAmount(amount); // All should be valid hex strings expect(specHex).toMatch(/^[0-9a-f]+$/); expect(termsHex).toMatch(/^[0-9a-f]+$/); expect(amountHex).toMatch(/^[0-9a-f]+$/); // Create global state const state = { assetSpec, contractTerms, amount }; const stateHex = RGB20Encoder.encodeGlobalState(state); expect(stateHex).toMatch(/^[0-9a-f]+$/); // Create complete genesis const genesis = RGB20Encoder.createGenesis({ assetSpec, contractTerms, amount, utxo: 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab:0' }); expect(genesis).toHaveProperty('schema_id'); expect(genesis).toHaveProperty('global_state'); expect(genesis).toHaveProperty('utxo'); // Genesis should be JSON serializable expect(() => JSON.stringify(genesis)).not.toThrow(); // And deterministic const genesis2 = RGB20Encoder.createGenesis({ assetSpec, contractTerms, amount, utxo: 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab:0' }); expect(JSON.stringify(genesis)).toBe(JSON.stringify(genesis2)); }); test('matches test vectors from specification', () => { // Test vector from STRICT-ENCODE-SPECIFICATION.md const spec = { ticker: 'NIATCKR', name: 'NIA asset name', precision: 8, details: null }; const encoded = RGB20Encoder.encodeAssetSpec(spec); expect(encoded).toBe('074e494154434b520e4e4941206173736574206e616d650800'); const terms = { text: 'NIA terms', media: null }; const termsEncoded = RGB20Encoder.encodeContractTerms(terms); expect(termsEncoded).toBe('094e4941207465726d7300'); const amountEncoded = RGB20Encoder.encodeAmount(1000000); expect(amountEncoded).toBe('40420f0000000000'); }); }); });