ea09516981
- Add ParseError class with context (offset, field, expected/actual) - transaction.js: parseRctSigPrunable throws instead of returning null - address.js: createAddress, toIntegratedAddress, toStandardAddress throw on invalid input - base58.js: decode functions throw descriptive errors - scanning.js: validate inputs and throw on failures - carrot-scanning.js: validate required fields - signature.js: remove debug console.log and testEd25519 - Update tests to expect throws for error cases
754 lines
23 KiB
JavaScript
754 lines
23 KiB
JavaScript
/**
|
|
* Address Generation Tests
|
|
*
|
|
* Tests for address creation, integrated addresses, and high-level subaddress generation.
|
|
*/
|
|
|
|
import {
|
|
generateSeed,
|
|
deriveKeys,
|
|
deriveCarrotKeys,
|
|
createAddress,
|
|
parseAddress,
|
|
toIntegratedAddress,
|
|
toStandardAddress,
|
|
generateRandomPaymentId,
|
|
createIntegratedAddressWithRandomId,
|
|
generateCNSubaddress,
|
|
generateCarrotSubaddress,
|
|
isValidAddress,
|
|
isMainnet,
|
|
isTestnet,
|
|
isStagenet,
|
|
isCarrot,
|
|
isLegacy,
|
|
isStandard,
|
|
isIntegrated,
|
|
isSubaddress,
|
|
bytesToHex,
|
|
hexToBytes,
|
|
seedToMnemonic,
|
|
mnemonicToSeed,
|
|
NETWORK,
|
|
ADDRESS_TYPE,
|
|
ADDRESS_FORMAT
|
|
} from '../src/index.js';
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
passed++;
|
|
} catch (error) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
function assertEqual(actual, expected, message = '') {
|
|
if (actual !== expected) {
|
|
throw new Error(`${message} Expected "${expected}", got "${actual}"`);
|
|
}
|
|
}
|
|
|
|
function assertNotEqual(actual, expected, message = '') {
|
|
if (actual === expected) {
|
|
throw new Error(`${message} Values should not be equal: ${actual}`);
|
|
}
|
|
}
|
|
|
|
function assertTrue(value, message = '') {
|
|
if (!value) {
|
|
throw new Error(`${message} Expected true, got ${value}`);
|
|
}
|
|
}
|
|
|
|
function assertFalse(value, message = '') {
|
|
if (value) {
|
|
throw new Error(`${message} Expected false, got ${value}`);
|
|
}
|
|
}
|
|
|
|
function assertLength(value, length, message = '') {
|
|
if (value.length !== length) {
|
|
throw new Error(`${message} Expected length ${length}, got ${value.length}`);
|
|
}
|
|
}
|
|
|
|
function assertThrows(fn, message = '') {
|
|
try {
|
|
fn();
|
|
throw new Error(`${message} Expected function to throw, but it did not`);
|
|
} catch (e) {
|
|
if (e.message.includes('Expected function to throw')) {
|
|
throw e;
|
|
}
|
|
// Function threw as expected
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Seed Generation Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Seed Generation Tests ---');
|
|
|
|
test('generateSeed returns 32 bytes', () => {
|
|
const seed = generateSeed();
|
|
assertLength(seed, 32);
|
|
});
|
|
|
|
test('generateSeed returns Uint8Array', () => {
|
|
const seed = generateSeed();
|
|
assertTrue(seed instanceof Uint8Array);
|
|
});
|
|
|
|
test('generateSeed produces different seeds each time', () => {
|
|
const seed1 = generateSeed();
|
|
const seed2 = generateSeed();
|
|
assertNotEqual(bytesToHex(seed1), bytesToHex(seed2));
|
|
});
|
|
|
|
test('generateSeed produces non-zero seeds', () => {
|
|
const seed = generateSeed();
|
|
let hasNonZero = false;
|
|
for (let i = 0; i < seed.length; i++) {
|
|
if (seed[i] !== 0) {
|
|
hasNonZero = true;
|
|
break;
|
|
}
|
|
}
|
|
assertTrue(hasNonZero, 'Seed should have non-zero bytes');
|
|
});
|
|
|
|
// ============================================================
|
|
// Address Creation Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Address Creation Tests ---');
|
|
|
|
const testSeed = generateSeed();
|
|
const testKeys = deriveKeys(testSeed);
|
|
|
|
test('createAddress creates valid mainnet legacy standard address', () => {
|
|
const addr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
assertTrue(addr !== null, 'Address should not be null');
|
|
assertTrue(isValidAddress(addr), 'Address should be valid');
|
|
assertTrue(isMainnet(addr), 'Should be mainnet');
|
|
assertTrue(isLegacy(addr), 'Should be legacy');
|
|
assertTrue(isStandard(addr), 'Should be standard');
|
|
});
|
|
|
|
test('createAddress creates valid mainnet CARROT standard address', () => {
|
|
const addr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.CARROT,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
assertTrue(addr !== null);
|
|
assertTrue(isValidAddress(addr));
|
|
assertTrue(isMainnet(addr));
|
|
assertTrue(isCarrot(addr));
|
|
assertTrue(isStandard(addr));
|
|
});
|
|
|
|
test('createAddress creates valid testnet address', () => {
|
|
const addr = createAddress({
|
|
network: NETWORK.TESTNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
assertTrue(addr !== null);
|
|
assertTrue(isValidAddress(addr));
|
|
assertTrue(isTestnet(addr));
|
|
});
|
|
|
|
test('createAddress creates valid stagenet address', () => {
|
|
const addr = createAddress({
|
|
network: NETWORK.STAGENET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
assertTrue(addr !== null);
|
|
assertTrue(isValidAddress(addr));
|
|
assertTrue(isStagenet(addr));
|
|
});
|
|
|
|
test('createAddress throws for invalid keys', () => {
|
|
assertThrows(() => {
|
|
createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: new Uint8Array(31), // Wrong length
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
});
|
|
});
|
|
|
|
test('createAddress round-trips correctly', () => {
|
|
const addr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const parsed = parseAddress(addr);
|
|
assertTrue(parsed.valid);
|
|
assertEqual(bytesToHex(parsed.spendPublicKey), bytesToHex(testKeys.spendPublicKey));
|
|
assertEqual(bytesToHex(parsed.viewPublicKey), bytesToHex(testKeys.viewPublicKey));
|
|
});
|
|
|
|
// ============================================================
|
|
// Integrated Address Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Integrated Address Tests ---');
|
|
|
|
test('toIntegratedAddress creates valid integrated address', () => {
|
|
const standardAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const paymentId = generateRandomPaymentId();
|
|
const integrated = toIntegratedAddress(standardAddr, paymentId);
|
|
|
|
assertTrue(integrated !== null);
|
|
assertTrue(isValidAddress(integrated));
|
|
assertTrue(isIntegrated(integrated));
|
|
assertTrue(isMainnet(integrated));
|
|
});
|
|
|
|
test('toIntegratedAddress accepts hex string payment ID', () => {
|
|
const standardAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const integrated = toIntegratedAddress(standardAddr, 'deadbeef12345678');
|
|
|
|
assertTrue(integrated !== null);
|
|
assertTrue(isIntegrated(integrated));
|
|
});
|
|
|
|
test('toIntegratedAddress preserves payment ID', () => {
|
|
const standardAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const paymentIdHex = 'deadbeef12345678';
|
|
const integrated = toIntegratedAddress(standardAddr, paymentIdHex);
|
|
const parsed = parseAddress(integrated);
|
|
|
|
assertEqual(bytesToHex(parsed.paymentId), paymentIdHex);
|
|
});
|
|
|
|
test('toIntegratedAddress throws for integrated address input', () => {
|
|
const standardAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const integrated = toIntegratedAddress(standardAddr, 'deadbeef12345678');
|
|
|
|
assertThrows(() => {
|
|
toIntegratedAddress(integrated, 'abcdef0123456789');
|
|
});
|
|
});
|
|
|
|
test('toStandardAddress extracts standard from integrated', () => {
|
|
const standardAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const integrated = toIntegratedAddress(standardAddr, 'deadbeef12345678');
|
|
const extracted = toStandardAddress(integrated);
|
|
|
|
assertEqual(extracted, standardAddr);
|
|
});
|
|
|
|
test('toStandardAddress throws for non-integrated', () => {
|
|
const standardAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
|
|
assertThrows(() => {
|
|
toStandardAddress(standardAddr);
|
|
});
|
|
});
|
|
|
|
test('createIntegratedAddressWithRandomId works', () => {
|
|
const standardAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const result = createIntegratedAddressWithRandomId(standardAddr);
|
|
|
|
assertTrue(result !== null);
|
|
assertTrue(result.address !== null);
|
|
assertTrue(isIntegrated(result.address));
|
|
assertLength(result.paymentId, 8);
|
|
assertLength(result.paymentIdHex, 16);
|
|
});
|
|
|
|
test('createIntegratedAddressWithRandomId generates unique payment IDs', () => {
|
|
const standardAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const result1 = createIntegratedAddressWithRandomId(standardAddr);
|
|
const result2 = createIntegratedAddressWithRandomId(standardAddr);
|
|
|
|
assertNotEqual(result1.paymentIdHex, result2.paymentIdHex);
|
|
});
|
|
|
|
// ============================================================
|
|
// High-Level Subaddress Generation Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- High-Level Subaddress Generation Tests ---');
|
|
|
|
test('generateCNSubaddress creates valid subaddress', () => {
|
|
const result = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
|
|
assertTrue(result !== null);
|
|
assertTrue(result.address !== null);
|
|
assertTrue(isValidAddress(result.address));
|
|
assertTrue(isSubaddress(result.address));
|
|
assertTrue(isLegacy(result.address));
|
|
assertLength(result.spendPublicKey, 32);
|
|
assertLength(result.viewPublicKey, 32);
|
|
assertEqual(result.major, 0);
|
|
assertEqual(result.minor, 1);
|
|
});
|
|
|
|
test('generateCNSubaddress (0,0) creates standard address', () => {
|
|
const result = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 0
|
|
});
|
|
|
|
// (0,0) should return original keys, but as subaddress type
|
|
assertEqual(bytesToHex(result.spendPublicKey), bytesToHex(testKeys.spendPublicKey));
|
|
});
|
|
|
|
test('generateCNSubaddress creates different addresses for different indices', () => {
|
|
const result1 = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
const result2 = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 2
|
|
});
|
|
|
|
assertNotEqual(result1.address, result2.address);
|
|
});
|
|
|
|
test('generateCarrotSubaddress creates valid CARROT subaddress', () => {
|
|
const carrotKeys = deriveCarrotKeys(testSeed);
|
|
const result = generateCarrotSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
accountSpendPubkey: testKeys.spendPublicKey,
|
|
accountViewPubkey: testKeys.viewPublicKey,
|
|
generateAddressSecret: hexToBytes(carrotKeys.generateAddressSecret),
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
|
|
assertTrue(result !== null);
|
|
assertTrue(result.address !== null);
|
|
assertTrue(isValidAddress(result.address));
|
|
assertTrue(isCarrot(result.address));
|
|
assertFalse(result.isMainAddress);
|
|
});
|
|
|
|
test('generateCarrotSubaddress (0,0) returns main address', () => {
|
|
const carrotKeys = deriveCarrotKeys(testSeed);
|
|
const result = generateCarrotSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
accountSpendPubkey: testKeys.spendPublicKey,
|
|
accountViewPubkey: testKeys.viewPublicKey,
|
|
generateAddressSecret: hexToBytes(carrotKeys.generateAddressSecret),
|
|
major: 0,
|
|
minor: 0
|
|
});
|
|
|
|
assertTrue(result.isMainAddress);
|
|
assertTrue(isStandard(result.address));
|
|
});
|
|
|
|
// ============================================================
|
|
// Full Wallet Generation Flow Test
|
|
// ============================================================
|
|
|
|
console.log('\n--- Full Wallet Generation Flow ---');
|
|
|
|
test('Complete wallet generation flow works', () => {
|
|
// 1. Generate seed
|
|
const seed = generateSeed();
|
|
assertLength(seed, 32);
|
|
|
|
// 2. Derive keys
|
|
const keys = deriveKeys(seed);
|
|
assertLength(keys.spendSecretKey, 32);
|
|
assertLength(keys.spendPublicKey, 32);
|
|
assertLength(keys.viewSecretKey, 32);
|
|
assertLength(keys.viewPublicKey, 32);
|
|
|
|
// 3. Create main address
|
|
const mainAddress = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewPublicKey: keys.viewPublicKey
|
|
});
|
|
assertTrue(isValidAddress(mainAddress));
|
|
assertTrue(isStandard(mainAddress));
|
|
|
|
// 4. Generate subaddress
|
|
const subaddress = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewSecretKey: keys.viewSecretKey,
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
assertTrue(isValidAddress(subaddress.address));
|
|
assertTrue(isSubaddress(subaddress.address));
|
|
|
|
// 5. Create integrated address
|
|
const integrated = createIntegratedAddressWithRandomId(mainAddress);
|
|
assertTrue(isValidAddress(integrated.address));
|
|
assertTrue(isIntegrated(integrated.address));
|
|
|
|
// 6. Verify addresses are on same network
|
|
assertTrue(isMainnet(mainAddress));
|
|
assertTrue(isMainnet(subaddress.address));
|
|
assertTrue(isMainnet(integrated.address));
|
|
});
|
|
|
|
test('Complete CARROT wallet generation flow works', () => {
|
|
// 1. Generate seed
|
|
const seed = generateSeed();
|
|
|
|
// 2. Derive keys
|
|
const keys = deriveKeys(seed);
|
|
const carrotKeys = deriveCarrotKeys(seed);
|
|
|
|
// 3. Create CARROT main address
|
|
const mainAddress = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.CARROT,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewPublicKey: keys.viewPublicKey
|
|
});
|
|
assertTrue(isValidAddress(mainAddress));
|
|
assertTrue(isCarrot(mainAddress));
|
|
|
|
// 4. Generate CARROT subaddress
|
|
const subaddress = generateCarrotSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
accountSpendPubkey: keys.spendPublicKey,
|
|
accountViewPubkey: keys.viewPublicKey,
|
|
generateAddressSecret: hexToBytes(carrotKeys.generateAddressSecret),
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
assertTrue(isValidAddress(subaddress.address));
|
|
assertTrue(isCarrot(subaddress.address));
|
|
});
|
|
|
|
// ============================================================
|
|
// Recovery & Round-Trip Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Recovery & Round-Trip Tests ---');
|
|
|
|
test('Seed -> Mnemonic -> Seed round-trip produces identical seed', () => {
|
|
const originalSeed = generateSeed();
|
|
const mnemonic = seedToMnemonic(originalSeed, { language: 'english' });
|
|
const result = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
|
|
assertTrue(result.valid, 'Mnemonic should be valid');
|
|
assertEqual(bytesToHex(result.seed), bytesToHex(originalSeed), 'Recovered seed should match original');
|
|
});
|
|
|
|
test('Seed -> Mnemonic -> Seed -> Keys produces identical keys', () => {
|
|
const originalSeed = generateSeed();
|
|
const originalKeys = deriveKeys(originalSeed);
|
|
|
|
// Convert to mnemonic and back
|
|
const mnemonic = seedToMnemonic(originalSeed, { language: 'english' });
|
|
const result = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
const recoveredKeys = deriveKeys(result.seed);
|
|
|
|
assertEqual(bytesToHex(recoveredKeys.spendSecretKey), bytesToHex(originalKeys.spendSecretKey));
|
|
assertEqual(bytesToHex(recoveredKeys.spendPublicKey), bytesToHex(originalKeys.spendPublicKey));
|
|
assertEqual(bytesToHex(recoveredKeys.viewSecretKey), bytesToHex(originalKeys.viewSecretKey));
|
|
assertEqual(bytesToHex(recoveredKeys.viewPublicKey), bytesToHex(originalKeys.viewPublicKey));
|
|
});
|
|
|
|
test('Seed -> Mnemonic -> Seed -> Address produces identical address', () => {
|
|
const originalSeed = generateSeed();
|
|
const originalKeys = deriveKeys(originalSeed);
|
|
const originalAddress = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: originalKeys.spendPublicKey,
|
|
viewPublicKey: originalKeys.viewPublicKey
|
|
});
|
|
|
|
// Recover from mnemonic
|
|
const mnemonic = seedToMnemonic(originalSeed, { language: 'english' });
|
|
const result = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
const recoveredKeys = deriveKeys(result.seed);
|
|
const recoveredAddress = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: recoveredKeys.spendPublicKey,
|
|
viewPublicKey: recoveredKeys.viewPublicKey
|
|
});
|
|
|
|
assertEqual(recoveredAddress, originalAddress, 'Recovered address should match original');
|
|
});
|
|
|
|
test('Full wallet recovery flow with subaddresses', () => {
|
|
// Original wallet
|
|
const originalSeed = generateSeed();
|
|
const originalKeys = deriveKeys(originalSeed);
|
|
const originalMain = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: originalKeys.spendPublicKey,
|
|
viewPublicKey: originalKeys.viewPublicKey
|
|
});
|
|
const originalSub = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: originalKeys.spendPublicKey,
|
|
viewSecretKey: originalKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 5
|
|
});
|
|
|
|
// Backup and recover
|
|
const mnemonic = seedToMnemonic(originalSeed, { language: 'english' });
|
|
|
|
// Simulate recovery on different device
|
|
const recoveryResult = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
assertTrue(recoveryResult.valid);
|
|
|
|
const recoveredKeys = deriveKeys(recoveryResult.seed);
|
|
const recoveredMain = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: recoveredKeys.spendPublicKey,
|
|
viewPublicKey: recoveredKeys.viewPublicKey
|
|
});
|
|
const recoveredSub = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: recoveredKeys.spendPublicKey,
|
|
viewSecretKey: recoveredKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 5
|
|
});
|
|
|
|
assertEqual(recoveredMain, originalMain, 'Main address should match');
|
|
assertEqual(recoveredSub.address, originalSub.address, 'Subaddress should match');
|
|
});
|
|
|
|
test('CARROT wallet recovery flow', () => {
|
|
// Original CARROT wallet
|
|
const originalSeed = generateSeed();
|
|
const originalKeys = deriveKeys(originalSeed);
|
|
const originalCarrotKeys = deriveCarrotKeys(originalSeed);
|
|
const originalAddress = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.CARROT,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: originalKeys.spendPublicKey,
|
|
viewPublicKey: originalKeys.viewPublicKey
|
|
});
|
|
|
|
// Backup and recover
|
|
const mnemonic = seedToMnemonic(originalSeed, { language: 'english' });
|
|
const recoveryResult = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
|
|
const recoveredKeys = deriveKeys(recoveryResult.seed);
|
|
const recoveredCarrotKeys = deriveCarrotKeys(recoveryResult.seed);
|
|
const recoveredAddress = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.CARROT,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: recoveredKeys.spendPublicKey,
|
|
viewPublicKey: recoveredKeys.viewPublicKey
|
|
});
|
|
|
|
assertEqual(recoveredAddress, originalAddress, 'CARROT address should match');
|
|
assertEqual(recoveredCarrotKeys.proveSpendKey, originalCarrotKeys.proveSpendKey);
|
|
assertEqual(recoveredCarrotKeys.viewBalanceSecret, originalCarrotKeys.viewBalanceSecret);
|
|
assertEqual(recoveredCarrotKeys.generateAddressSecret, originalCarrotKeys.generateAddressSecret);
|
|
});
|
|
|
|
test('Mnemonic recovery works across all supported languages', () => {
|
|
const originalSeed = generateSeed();
|
|
const originalKeys = deriveKeys(originalSeed);
|
|
const originalAddress = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: originalKeys.spendPublicKey,
|
|
viewPublicKey: originalKeys.viewPublicKey
|
|
});
|
|
|
|
// Test a selection of languages
|
|
const testLanguages = ['english', 'spanish', 'french', 'italian', 'portuguese'];
|
|
|
|
for (const lang of testLanguages) {
|
|
const mnemonic = seedToMnemonic(originalSeed, { language: lang });
|
|
const result = mnemonicToSeed(mnemonic, { language: lang });
|
|
|
|
assertTrue(result.valid, `${lang} mnemonic should be valid`);
|
|
|
|
const recoveredKeys = deriveKeys(result.seed);
|
|
const recoveredAddress = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: recoveredKeys.spendPublicKey,
|
|
viewPublicKey: recoveredKeys.viewPublicKey
|
|
});
|
|
|
|
assertEqual(recoveredAddress, originalAddress, `${lang} recovery should produce same address`);
|
|
}
|
|
});
|
|
|
|
test('Address parsing and recreation produces identical address', () => {
|
|
const seed = generateSeed();
|
|
const keys = deriveKeys(seed);
|
|
const original = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewPublicKey: keys.viewPublicKey
|
|
});
|
|
|
|
// Parse it
|
|
const parsed = parseAddress(original);
|
|
assertTrue(parsed.valid);
|
|
|
|
// Recreate it from parsed components
|
|
const recreated = createAddress({
|
|
network: parsed.network,
|
|
format: parsed.format,
|
|
type: parsed.type,
|
|
spendPublicKey: parsed.spendPublicKey,
|
|
viewPublicKey: parsed.viewPublicKey
|
|
});
|
|
|
|
assertEqual(recreated, original);
|
|
});
|
|
|
|
test('Integrated address round-trip preserves all data', () => {
|
|
const seed = generateSeed();
|
|
const keys = deriveKeys(seed);
|
|
const standard = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: keys.spendPublicKey,
|
|
viewPublicKey: keys.viewPublicKey
|
|
});
|
|
|
|
const paymentId = 'abcdef0123456789';
|
|
const integrated = toIntegratedAddress(standard, paymentId);
|
|
|
|
// Parse integrated
|
|
const parsed = parseAddress(integrated);
|
|
assertTrue(parsed.valid);
|
|
assertEqual(parsed.type, ADDRESS_TYPE.INTEGRATED);
|
|
assertEqual(bytesToHex(parsed.paymentId), paymentId);
|
|
|
|
// Extract standard and verify
|
|
const extractedStandard = toStandardAddress(integrated);
|
|
assertEqual(extractedStandard, standard);
|
|
});
|
|
|
|
// ============================================================
|
|
// Summary
|
|
// ============================================================
|
|
|
|
console.log('\n--- Address Test Summary ---');
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
console.log(`Total: ${passed + failed}`);
|
|
|
|
if (failed > 0) {
|
|
console.log('\n⚠️ Some tests failed!');
|
|
process.exit(1);
|
|
} else {
|
|
console.log('\n✓ All address tests passed!');
|
|
}
|