43e1ebbef8
- Create src/oracle.js with complete pricing record support - Data structures: PricingRecord, AssetData, SupplyData (matching C++ pricing_record.h) - DSA/ECDSA signature verification with mainnet/testnet public keys - Conversion rate calculation with rounding (getConversionRate, getConvertedAmount) - Slippage calculation (1/32 = 3.125%) with refund logic - Pricing record validation (HF gates, timestamps, signatures) - JSON serialization/parsing for oracle responses - HTTP client for fetching pricing records from oracle server - Add 66 unit tests in test/oracle.test.js - Export oracle module from index.js
817 lines
24 KiB
JavaScript
817 lines
24 KiB
JavaScript
#!/usr/bin/env bun
|
|
/**
|
|
* Oracle/Pricing Module Tests
|
|
*
|
|
* Tests for Salvium's oracle pricing system including:
|
|
* - Pricing record data structures
|
|
* - Signature verification
|
|
* - Conversion rate calculation
|
|
* - Slippage calculation
|
|
* - Serialization/parsing
|
|
*
|
|
* Reference: ~/github/salvium/src/oracle/pricing_record.h
|
|
* ~/github/salvium/src/oracle/pricing_record.cpp
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import {
|
|
// Constants
|
|
COIN,
|
|
PRICING_RECORD_VALID_BLOCKS,
|
|
PRICING_RECORD_VALID_TIME_DIFF_FROM_BLOCK,
|
|
CONVERSION_RATE_ROUNDING,
|
|
ASSET_TYPES,
|
|
HF_VERSION_ENABLE_ORACLE,
|
|
HF_VERSION_SLIPPAGE_YIELD,
|
|
|
|
// Data structures
|
|
createEmptyPricingRecord,
|
|
isPricingRecordEmpty,
|
|
getAssetPrice,
|
|
getAssetMaPrice,
|
|
|
|
// Signature verification
|
|
buildSignatureMessage,
|
|
getOraclePublicKey,
|
|
|
|
// Validation
|
|
validatePricingRecord,
|
|
|
|
// Conversion
|
|
getConversionRate,
|
|
getConvertedAmount,
|
|
calculateSlippage,
|
|
calculateConversion,
|
|
|
|
// Serialization
|
|
parsePricingRecordFromJson,
|
|
pricingRecordToJson
|
|
} from '../src/oracle.js';
|
|
|
|
|
|
// ============================================================================
|
|
// TEST DATA
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Create a mock pricing record for testing
|
|
*/
|
|
function createMockPricingRecord() {
|
|
return {
|
|
prVersion: 1,
|
|
height: 100000,
|
|
supply: {
|
|
sal: 1000000000000000n, // 10M SAL
|
|
vsd: 500000000000000n // 5M VSD
|
|
},
|
|
assets: [
|
|
{ assetType: 'SAL', spotPrice: 100000000n, maPrice: 100000000n }, // 1.0 price
|
|
{ assetType: 'VSD', spotPrice: 200000000n, maPrice: 195000000n } // 2.0 price
|
|
],
|
|
timestamp: Math.floor(Date.now() / 1000),
|
|
signature: new Uint8Array(64).fill(0) // Dummy signature
|
|
};
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// CONSTANTS TESTS
|
|
// ============================================================================
|
|
|
|
describe('Oracle Constants', () => {
|
|
|
|
test('COIN equals 10^8', () => {
|
|
expect(COIN).toBe(100000000n);
|
|
});
|
|
|
|
test('PRICING_RECORD_VALID_BLOCKS equals 10', () => {
|
|
expect(PRICING_RECORD_VALID_BLOCKS).toBe(10);
|
|
});
|
|
|
|
test('PRICING_RECORD_VALID_TIME_DIFF_FROM_BLOCK equals 120 seconds', () => {
|
|
expect(PRICING_RECORD_VALID_TIME_DIFF_FROM_BLOCK).toBe(120);
|
|
});
|
|
|
|
test('CONVERSION_RATE_ROUNDING equals 10000', () => {
|
|
expect(CONVERSION_RATE_ROUNDING).toBe(10000n);
|
|
});
|
|
|
|
test('ASSET_TYPES includes SAL, SAL1, BURN', () => {
|
|
expect(ASSET_TYPES).toContain('SAL');
|
|
expect(ASSET_TYPES).toContain('SAL1');
|
|
expect(ASSET_TYPES).toContain('BURN');
|
|
});
|
|
|
|
test('HF_VERSION_ENABLE_ORACLE equals 255', () => {
|
|
expect(HF_VERSION_ENABLE_ORACLE).toBe(255);
|
|
});
|
|
|
|
test('HF_VERSION_SLIPPAGE_YIELD equals 255', () => {
|
|
expect(HF_VERSION_SLIPPAGE_YIELD).toBe(255);
|
|
});
|
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
// DATA STRUCTURE TESTS
|
|
// ============================================================================
|
|
|
|
describe('Pricing Record Data Structures', () => {
|
|
|
|
describe('createEmptyPricingRecord', () => {
|
|
|
|
test('creates empty pricing record with all zero values', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
|
|
expect(pr.prVersion).toBe(0);
|
|
expect(pr.height).toBe(0);
|
|
expect(pr.supply.sal).toBe(0n);
|
|
expect(pr.supply.vsd).toBe(0n);
|
|
expect(pr.assets).toEqual([]);
|
|
expect(pr.timestamp).toBe(0);
|
|
expect(pr.signature.length).toBe(0);
|
|
});
|
|
|
|
});
|
|
|
|
describe('isPricingRecordEmpty', () => {
|
|
|
|
test('returns true for empty pricing record', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
expect(isPricingRecordEmpty(pr)).toBe(true);
|
|
});
|
|
|
|
test('returns false for pricing record with version', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
pr.prVersion = 1;
|
|
expect(isPricingRecordEmpty(pr)).toBe(false);
|
|
});
|
|
|
|
test('returns false for pricing record with height', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
pr.height = 100;
|
|
expect(isPricingRecordEmpty(pr)).toBe(false);
|
|
});
|
|
|
|
test('returns false for pricing record with supply', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
pr.supply.sal = 1000n;
|
|
expect(isPricingRecordEmpty(pr)).toBe(false);
|
|
});
|
|
|
|
test('returns false for pricing record with assets', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
pr.assets = [{ assetType: 'SAL', spotPrice: 100n, maPrice: 100n }];
|
|
expect(isPricingRecordEmpty(pr)).toBe(false);
|
|
});
|
|
|
|
test('returns false for pricing record with timestamp', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
pr.timestamp = 12345;
|
|
expect(isPricingRecordEmpty(pr)).toBe(false);
|
|
});
|
|
|
|
test('returns false for pricing record with signature', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
pr.signature = new Uint8Array([1, 2, 3]);
|
|
expect(isPricingRecordEmpty(pr)).toBe(false);
|
|
});
|
|
|
|
test('returns false for full pricing record', () => {
|
|
const pr = createMockPricingRecord();
|
|
expect(isPricingRecordEmpty(pr)).toBe(false);
|
|
});
|
|
|
|
});
|
|
|
|
describe('getAssetPrice', () => {
|
|
|
|
test('returns spot price for existing asset', () => {
|
|
const pr = createMockPricingRecord();
|
|
expect(getAssetPrice(pr, 'SAL')).toBe(100000000n);
|
|
expect(getAssetPrice(pr, 'VSD')).toBe(200000000n);
|
|
});
|
|
|
|
test('returns 0n for non-existent asset', () => {
|
|
const pr = createMockPricingRecord();
|
|
expect(getAssetPrice(pr, 'UNKNOWN')).toBe(0n);
|
|
});
|
|
|
|
test('returns 0n for empty pricing record', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
expect(getAssetPrice(pr, 'SAL')).toBe(0n);
|
|
});
|
|
|
|
});
|
|
|
|
describe('getAssetMaPrice', () => {
|
|
|
|
test('returns moving average price for existing asset', () => {
|
|
const pr = createMockPricingRecord();
|
|
expect(getAssetMaPrice(pr, 'SAL')).toBe(100000000n);
|
|
expect(getAssetMaPrice(pr, 'VSD')).toBe(195000000n);
|
|
});
|
|
|
|
test('returns 0n for non-existent asset', () => {
|
|
const pr = createMockPricingRecord();
|
|
expect(getAssetMaPrice(pr, 'UNKNOWN')).toBe(0n);
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
// SIGNATURE VERIFICATION TESTS
|
|
// ============================================================================
|
|
|
|
describe('Signature Verification', () => {
|
|
|
|
describe('buildSignatureMessage', () => {
|
|
|
|
test('builds correct JSON message', () => {
|
|
const pr = {
|
|
prVersion: 1,
|
|
height: 100,
|
|
supply: { sal: 1000n, vsd: 500n },
|
|
assets: [
|
|
{ assetType: 'SAL', spotPrice: 100n, maPrice: 100n }
|
|
],
|
|
timestamp: 1234567890,
|
|
signature: new Uint8Array(0)
|
|
};
|
|
|
|
const message = buildSignatureMessage(pr);
|
|
const parsed = JSON.parse(message);
|
|
|
|
expect(parsed.pr_version).toBe(1);
|
|
expect(parsed.height).toBe(100);
|
|
expect(parsed.supply.SAL).toBe(1000);
|
|
expect(parsed.supply.VSD).toBe(500);
|
|
expect(parsed.assets.length).toBe(1);
|
|
expect(parsed.assets[0].asset_type).toBe('SAL');
|
|
expect(parsed.timestamp).toBe(1234567890);
|
|
});
|
|
|
|
test('produces compact JSON without whitespace', () => {
|
|
const pr = createMockPricingRecord();
|
|
const message = buildSignatureMessage(pr);
|
|
|
|
// Should not contain formatting whitespace
|
|
expect(message).not.toMatch(/ /); // No double spaces
|
|
expect(message).not.toContain('\n');
|
|
expect(message).not.toContain('\t');
|
|
});
|
|
|
|
});
|
|
|
|
describe('getOraclePublicKey', () => {
|
|
|
|
test('returns mainnet key by default', () => {
|
|
const key = getOraclePublicKey();
|
|
expect(key).toContain('BEGIN PUBLIC KEY');
|
|
expect(key).toContain('MIIDRDCCAjYGByqGSM44BAE'); // DSA key
|
|
});
|
|
|
|
test('returns mainnet key explicitly', () => {
|
|
const key = getOraclePublicKey('mainnet');
|
|
expect(key).toContain('BEGIN PUBLIC KEY');
|
|
expect(key).toContain('MIIDRDCCAjYGByqGSM44BAE'); // DSA key
|
|
});
|
|
|
|
test('returns testnet key', () => {
|
|
const key = getOraclePublicKey('testnet');
|
|
expect(key).toContain('BEGIN PUBLIC KEY');
|
|
expect(key).toContain('MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQg'); // ECDSA key
|
|
});
|
|
|
|
test('returns testnet key for stagenet', () => {
|
|
const key = getOraclePublicKey('stagenet');
|
|
expect(key).toContain('BEGIN PUBLIC KEY');
|
|
expect(key).toContain('MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQg'); // ECDSA key
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
// VALIDATION TESTS
|
|
// ============================================================================
|
|
|
|
describe('Pricing Record Validation', () => {
|
|
|
|
describe('validatePricingRecord', () => {
|
|
|
|
test('empty record is always valid', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
const result = validatePricingRecord(pr);
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
test('non-empty record requires HF >= HF_VERSION_SLIPPAGE_YIELD', () => {
|
|
const pr = createMockPricingRecord();
|
|
|
|
// Before HF
|
|
const result1 = validatePricingRecord(pr, { hfVersion: 5 });
|
|
expect(result1.valid).toBe(false);
|
|
expect(result1.error).toContain('not allowed before oracle HF');
|
|
|
|
// At HF (would fail signature, but that's a different error)
|
|
// Just testing the HF gate here
|
|
});
|
|
|
|
test('rejects timestamp too far in future', () => {
|
|
const pr = createMockPricingRecord();
|
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
|
|
// Set timestamp 200 seconds in future (> 120 seconds allowed)
|
|
pr.timestamp = currentTime + 200;
|
|
|
|
const result = validatePricingRecord(pr, {
|
|
hfVersion: 255,
|
|
blockTimestamp: currentTime
|
|
});
|
|
|
|
// Note: This will also fail signature verification first,
|
|
// but if we had a valid signature, timestamp would be checked
|
|
});
|
|
|
|
test('rejects timestamp older than previous block', () => {
|
|
const pr = createMockPricingRecord();
|
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
|
|
// Set timestamp older than last block
|
|
pr.timestamp = currentTime - 100;
|
|
|
|
const result = validatePricingRecord(pr, {
|
|
hfVersion: 255,
|
|
blockTimestamp: currentTime,
|
|
lastBlockTimestamp: currentTime - 50 // Previous block was 50 seconds ago
|
|
});
|
|
|
|
// Would fail signature first, but tests the logic
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
// CONVERSION TESTS
|
|
// ============================================================================
|
|
|
|
describe('Conversion Rate Calculation', () => {
|
|
|
|
describe('getConversionRate', () => {
|
|
|
|
test('same asset returns 1:1 rate (COIN)', () => {
|
|
const pr = createMockPricingRecord();
|
|
|
|
const result = getConversionRate(pr, 'SAL', 'SAL');
|
|
expect(result.success).toBe(true);
|
|
expect(result.rate).toBe(COIN);
|
|
});
|
|
|
|
test('rejects conversion to BURN', () => {
|
|
const pr = createMockPricingRecord();
|
|
|
|
const result = getConversionRate(pr, 'SAL', 'BURN');
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Cannot convert to BURN');
|
|
});
|
|
|
|
test('SAL -> VSD with 1:2 price ratio returns 0.5 rate', () => {
|
|
const pr = createMockPricingRecord();
|
|
// SAL price = 1.0, VSD price = 2.0
|
|
// Rate = 1.0 / 2.0 = 0.5 (50000000 in COIN units)
|
|
|
|
const result = getConversionRate(pr, 'SAL', 'VSD');
|
|
expect(result.success).toBe(true);
|
|
|
|
// Rate should be around 50000000 (0.5 COIN), rounded down to nearest 10000
|
|
const expectedRate = 50000000n - (50000000n % CONVERSION_RATE_ROUNDING);
|
|
expect(result.rate).toBe(expectedRate);
|
|
});
|
|
|
|
test('VSD -> SAL with 2:1 price ratio returns 2.0 rate', () => {
|
|
const pr = createMockPricingRecord();
|
|
// VSD price = 2.0, SAL price = 1.0
|
|
// Rate = 2.0 / 1.0 = 2.0 (200000000 in COIN units)
|
|
|
|
const result = getConversionRate(pr, 'VSD', 'SAL');
|
|
expect(result.success).toBe(true);
|
|
|
|
// Rate should be around 200000000 (2.0 COIN), rounded down to nearest 10000
|
|
const expectedRate = 200000000n - (200000000n % CONVERSION_RATE_ROUNDING);
|
|
expect(result.rate).toBe(expectedRate);
|
|
});
|
|
|
|
test('rejects invalid conversion pair SAL -> SAL1', () => {
|
|
const pr = createMockPricingRecord();
|
|
|
|
const result = getConversionRate(pr, 'SAL', 'SAL1');
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Invalid conversion pair');
|
|
});
|
|
|
|
test('rejects when price data is missing', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
|
|
const result = getConversionRate(pr, 'SAL', 'VSD');
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Missing price data');
|
|
});
|
|
|
|
test('rounds rate down to nearest 10000', () => {
|
|
const pr = {
|
|
...createMockPricingRecord(),
|
|
assets: [
|
|
{ assetType: 'SAL', spotPrice: 100000001n, maPrice: 100000001n },
|
|
{ assetType: 'VSD', spotPrice: 100000000n, maPrice: 100000000n }
|
|
]
|
|
};
|
|
|
|
const result = getConversionRate(pr, 'SAL', 'VSD');
|
|
expect(result.success).toBe(true);
|
|
|
|
// Should be rounded to nearest 10000
|
|
expect(result.rate % CONVERSION_RATE_ROUNDING).toBe(0n);
|
|
});
|
|
|
|
});
|
|
|
|
describe('getConvertedAmount', () => {
|
|
|
|
test('calculates correct converted amount', () => {
|
|
// 1 SAL with 1:1 rate should give 1 SAL
|
|
const result = getConvertedAmount(COIN, COIN);
|
|
expect(result.success).toBe(true);
|
|
expect(result.amount).toBe(COIN);
|
|
});
|
|
|
|
test('calculates with 2:1 rate', () => {
|
|
// 1 SAL with 2:1 rate should give 2 SAL
|
|
const rate = 200000000n; // 2.0 in COIN units
|
|
const amount = COIN; // 1.0
|
|
|
|
const result = getConvertedAmount(rate, amount);
|
|
expect(result.success).toBe(true);
|
|
expect(result.amount).toBe(200000000n); // 2.0 SAL
|
|
});
|
|
|
|
test('calculates with 0.5:1 rate', () => {
|
|
// 1 SAL with 0.5:1 rate should give 0.5 SAL
|
|
const rate = 50000000n; // 0.5 in COIN units
|
|
const amount = COIN; // 1.0
|
|
|
|
const result = getConvertedAmount(rate, amount);
|
|
expect(result.success).toBe(true);
|
|
expect(result.amount).toBe(50000000n); // 0.5 SAL
|
|
});
|
|
|
|
test('rejects zero rate', () => {
|
|
const result = getConvertedAmount(0n, COIN);
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Invalid conversion rate');
|
|
});
|
|
|
|
test('rejects zero amount', () => {
|
|
const result = getConvertedAmount(COIN, 0n);
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Invalid source amount');
|
|
});
|
|
|
|
});
|
|
|
|
describe('calculateSlippage', () => {
|
|
|
|
test('calculates 1/32 (3.125%) of amount', () => {
|
|
// 3200 SAL should give 100 SAL slippage (3200 / 32 = 100)
|
|
const amount = 320000000000n; // 3200 SAL
|
|
const slippage = calculateSlippage(amount);
|
|
expect(slippage).toBe(10000000000n); // 100 SAL
|
|
});
|
|
|
|
test('uses bit shift for efficiency', () => {
|
|
// 32 SAL -> 1 SAL slippage
|
|
const amount = 3200000000n; // 32 SAL
|
|
const slippage = calculateSlippage(amount);
|
|
expect(slippage).toBe(100000000n); // 1 SAL
|
|
});
|
|
|
|
test('handles small amounts correctly', () => {
|
|
// Very small amount
|
|
const amount = 32n;
|
|
const slippage = calculateSlippage(amount);
|
|
expect(slippage).toBe(1n);
|
|
});
|
|
|
|
test('handles amount less than 32', () => {
|
|
// Amount less than 32 results in 0 slippage
|
|
const amount = 31n;
|
|
const slippage = calculateSlippage(amount);
|
|
expect(slippage).toBe(0n);
|
|
});
|
|
|
|
});
|
|
|
|
describe('calculateConversion', () => {
|
|
|
|
test('calculates full conversion with slippage', () => {
|
|
const pr = createMockPricingRecord();
|
|
const amount = 3200000000000n; // 32000 SAL
|
|
|
|
const result = calculateConversion(pr, 'SAL', 'VSD', amount, 1000000000000n);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.actualSlippage).toBe(100000000000n); // 1000 SAL (3.125%)
|
|
expect(result.amountMinted).toBeDefined();
|
|
expect(result.conversionRate).toBeDefined();
|
|
});
|
|
|
|
test('triggers refund when slippage exceeds limit', () => {
|
|
const pr = createMockPricingRecord();
|
|
const amount = 3200000000000n; // 32000 SAL
|
|
const slippageLimit = 1n; // Unrealistically low limit
|
|
|
|
const result = calculateConversion(pr, 'SAL', 'VSD', amount, slippageLimit);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.refund).toBe(true);
|
|
expect(result.amountMinted).toBe(0n);
|
|
expect(result.error).toContain('Slippage limit exceeded');
|
|
});
|
|
|
|
test('rejects same asset conversion', () => {
|
|
const pr = createMockPricingRecord();
|
|
|
|
const result = calculateConversion(pr, 'SAL', 'SAL', COIN, COIN);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('source and dest assets are identical');
|
|
});
|
|
|
|
test('rejects missing source asset', () => {
|
|
const pr = createMockPricingRecord();
|
|
|
|
const result = calculateConversion(pr, null, 'VSD', COIN, COIN);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Source asset not provided');
|
|
});
|
|
|
|
test('rejects missing destination asset', () => {
|
|
const pr = createMockPricingRecord();
|
|
|
|
const result = calculateConversion(pr, 'SAL', null, COIN, COIN);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Destination asset not provided');
|
|
});
|
|
|
|
test('propagates invalid conversion pair error', () => {
|
|
const pr = createMockPricingRecord();
|
|
|
|
const result = calculateConversion(pr, 'SAL', 'SAL1', COIN, COIN);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Invalid conversion pair');
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
// SERIALIZATION TESTS
|
|
// ============================================================================
|
|
|
|
describe('Pricing Record Serialization', () => {
|
|
|
|
describe('parsePricingRecordFromJson', () => {
|
|
|
|
test('parses complete pricing record from JSON', () => {
|
|
const json = {
|
|
pr_version: 1,
|
|
height: 100000,
|
|
supply: {
|
|
SAL: 1000000000000000,
|
|
VSD: 500000000000000
|
|
},
|
|
assets: [
|
|
{ asset_type: 'SAL', spot_price: 100000000, ma_price: 100000000 },
|
|
{ asset_type: 'VSD', spot_price: 200000000, ma_price: 195000000 }
|
|
],
|
|
timestamp: 1234567890,
|
|
signature: 'aabbccdd'
|
|
};
|
|
|
|
const pr = parsePricingRecordFromJson(json);
|
|
|
|
expect(pr.prVersion).toBe(1);
|
|
expect(pr.height).toBe(100000);
|
|
expect(pr.supply.sal).toBe(1000000000000000n);
|
|
expect(pr.supply.vsd).toBe(500000000000000n);
|
|
expect(pr.assets.length).toBe(2);
|
|
expect(pr.assets[0].assetType).toBe('SAL');
|
|
expect(pr.assets[0].spotPrice).toBe(100000000n);
|
|
expect(pr.timestamp).toBe(1234567890);
|
|
expect(pr.signature.length).toBe(4); // 'aabbccdd' = 4 bytes
|
|
});
|
|
|
|
test('handles lowercase supply keys', () => {
|
|
const json = {
|
|
pr_version: 1,
|
|
height: 100,
|
|
supply: {
|
|
sal: 1000,
|
|
vsd: 500
|
|
},
|
|
assets: [],
|
|
timestamp: 0,
|
|
signature: ''
|
|
};
|
|
|
|
const pr = parsePricingRecordFromJson(json);
|
|
|
|
expect(pr.supply.sal).toBe(1000n);
|
|
expect(pr.supply.vsd).toBe(500n);
|
|
});
|
|
|
|
test('handles missing optional fields', () => {
|
|
const json = {
|
|
height: 100
|
|
};
|
|
|
|
const pr = parsePricingRecordFromJson(json);
|
|
|
|
expect(pr.prVersion).toBe(0);
|
|
expect(pr.height).toBe(100);
|
|
expect(pr.supply.sal).toBe(0n);
|
|
expect(pr.supply.vsd).toBe(0n);
|
|
expect(pr.assets).toEqual([]);
|
|
expect(pr.timestamp).toBe(0);
|
|
expect(pr.signature.length).toBe(0);
|
|
});
|
|
|
|
test('handles signature as array', () => {
|
|
const json = {
|
|
pr_version: 1,
|
|
height: 100,
|
|
supply: { SAL: 0, VSD: 0 },
|
|
assets: [],
|
|
timestamp: 0,
|
|
signature: [0xaa, 0xbb, 0xcc]
|
|
};
|
|
|
|
const pr = parsePricingRecordFromJson(json);
|
|
|
|
expect(pr.signature.length).toBe(3);
|
|
expect(pr.signature[0]).toBe(0xaa);
|
|
expect(pr.signature[1]).toBe(0xbb);
|
|
expect(pr.signature[2]).toBe(0xcc);
|
|
});
|
|
|
|
test('handles 0x prefixed hex signature', () => {
|
|
const json = {
|
|
pr_version: 1,
|
|
height: 100,
|
|
supply: { SAL: 0, VSD: 0 },
|
|
assets: [],
|
|
timestamp: 0,
|
|
signature: '0xaabbcc'
|
|
};
|
|
|
|
const pr = parsePricingRecordFromJson(json);
|
|
|
|
expect(pr.signature.length).toBe(3);
|
|
});
|
|
|
|
});
|
|
|
|
describe('pricingRecordToJson', () => {
|
|
|
|
test('converts pricing record to JSON', () => {
|
|
const pr = createMockPricingRecord();
|
|
const json = pricingRecordToJson(pr);
|
|
|
|
expect(json.pr_version).toBe(1);
|
|
expect(json.height).toBe(100000);
|
|
expect(json.supply.SAL).toBe('1000000000000000');
|
|
expect(json.supply.VSD).toBe('500000000000000');
|
|
expect(json.assets.length).toBe(2);
|
|
expect(json.assets[0].asset_type).toBe('SAL');
|
|
expect(json.assets[0].spot_price).toBe('100000000');
|
|
expect(json.timestamp).toBe(pr.timestamp);
|
|
expect(typeof json.signature).toBe('string');
|
|
});
|
|
|
|
test('handles empty signature', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
const json = pricingRecordToJson(pr);
|
|
|
|
expect(json.signature).toBe('');
|
|
});
|
|
|
|
test('converts signature to hex string', () => {
|
|
const pr = createEmptyPricingRecord();
|
|
pr.signature = new Uint8Array([0xaa, 0xbb, 0xcc]);
|
|
|
|
const json = pricingRecordToJson(pr);
|
|
|
|
expect(json.signature).toBe('aabbcc');
|
|
});
|
|
|
|
});
|
|
|
|
describe('round-trip serialization', () => {
|
|
|
|
test('JSON -> PricingRecord -> JSON preserves data', () => {
|
|
const originalJson = {
|
|
pr_version: 1,
|
|
height: 100000,
|
|
supply: { SAL: '1000000000000', VSD: '500000000000' },
|
|
assets: [
|
|
{ asset_type: 'SAL', spot_price: '100000000', ma_price: '100000000' }
|
|
],
|
|
timestamp: 1234567890,
|
|
signature: 'aabbccdd'
|
|
};
|
|
|
|
const pr = parsePricingRecordFromJson(originalJson);
|
|
const resultJson = pricingRecordToJson(pr);
|
|
|
|
expect(resultJson.pr_version).toBe(originalJson.pr_version);
|
|
expect(resultJson.height).toBe(originalJson.height);
|
|
expect(resultJson.supply.SAL).toBe(originalJson.supply.SAL);
|
|
expect(resultJson.supply.VSD).toBe(originalJson.supply.VSD);
|
|
expect(resultJson.assets.length).toBe(originalJson.assets.length);
|
|
expect(resultJson.timestamp).toBe(originalJson.timestamp);
|
|
expect(resultJson.signature).toBe(originalJson.signature);
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
// ============================================================================
|
|
// EDGE CASES
|
|
// ============================================================================
|
|
|
|
describe('Edge Cases', () => {
|
|
|
|
test('handles very large amounts in conversion', () => {
|
|
const pr = createMockPricingRecord();
|
|
const largeAmount = 1000000000000000000n; // 10B SAL
|
|
|
|
const result = calculateConversion(pr, 'SAL', 'VSD', largeAmount, largeAmount);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.amountMinted).toBeDefined();
|
|
});
|
|
|
|
test('handles minimum conversion amount', () => {
|
|
const pr = createMockPricingRecord();
|
|
const minAmount = 1n; // 1 atomic unit
|
|
|
|
const rateResult = getConversionRate(pr, 'SAL', 'VSD');
|
|
expect(rateResult.success).toBe(true);
|
|
|
|
const convertResult = getConvertedAmount(rateResult.rate, minAmount);
|
|
// May be 0 due to precision, but should not error
|
|
expect(convertResult.success).toBe(true);
|
|
});
|
|
|
|
test('slippage calculation handles exact multiples of 32', () => {
|
|
const amount = 320n; // Exactly 10 * 32
|
|
const slippage = calculateSlippage(amount);
|
|
expect(slippage).toBe(10n);
|
|
});
|
|
|
|
test('conversion rate handles equal prices', () => {
|
|
const pr = {
|
|
...createMockPricingRecord(),
|
|
assets: [
|
|
{ assetType: 'SAL', spotPrice: 100000000n, maPrice: 100000000n },
|
|
{ assetType: 'VSD', spotPrice: 100000000n, maPrice: 100000000n } // Same price
|
|
]
|
|
};
|
|
|
|
const result = getConversionRate(pr, 'SAL', 'VSD');
|
|
expect(result.success).toBe(true);
|
|
// 1:1 rate
|
|
expect(result.rate).toBe(COIN - (COIN % CONVERSION_RATE_ROUNDING));
|
|
});
|
|
|
|
});
|
|
|
|
|
|
console.log('\n=== Oracle/Pricing Module Tests ===\n');
|