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