UNPKG

@aladas-org/cryptocalc

Version:
669 lines (515 loc) 24.2 kB
/** * ============================================================================ * Unit Tests - BIP32 Utilities * ============================================================================ * Tests the BIP32 utility functions for HD wallet generation * Location: www/js/crypto/HDWallet/bip32_utils.js * Note: Console.log suppression is configured globally in setup.js * ============================================================================ */ // Import required modules const { Bip32Utils } = require('@crypto/HDWallet/bip32_utils.js'); const { Bip39Utils } = require('@crypto/bip39_utils.js'); // Import PrettyLog and log mode constant to disable console.log from production code const { PrettyLog, UNIT_TESTS_LOG_MODE } = require('@util/log/log_utils.js'); // Import blockchain constants const { BITCOIN, ETHEREUM, DOGECOIN, LITECOIN, SOLANA, AVALANCHE, POLYGON, CARDANO, SUI, ETHEREUM_CLASSIC, STELLAR, RIPPLE, TRON, BITCOIN_CASH, BITCOIN_SV, RAVENCOIN, VECHAIN, DASH, FIRO, BINANCE_BSC, HORIZEN, TERRA_LUNA, COIN, COIN_TYPE, COIN_TYPES, COIN_ABBREVIATIONS, MAINNET, TESTNET } = require('@crypto/const_blockchains.js'); // Import wallet property constants const { NULL_HEX, ADDRESS, PRIVATE_KEY, PUBLIC_KEY_HEX, CRYPTO_NET, MASTER_SEED, MASTER_PK_HEX, CHAINCODE, BIP32_ROOT_KEY, ACCOUNT_XPRIV, ACCOUNT_XPUB, PRIV_KEY } = require('@crypto/const_wallet.js'); // Import keyword constants const { BLOCKCHAIN, NULL_BLOCKCHAIN, MNEMONICS, UUID, WIF, BIP32_PROTOCOL, BIP32_PASSPHRASE, ACCOUNT, ADDRESS_INDEX, DERIVATION_PATH } = require('@www/js/const_keywords.js'); describe('BIP32 Utilities', () => { // Test data const TEST_MNEMONICS = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; const TEST_MNEMONICS_12 = "much bottom such hurt hunt welcome cushion erosion pulse admit name deer"; beforeAll(() => { // Disable console.log from pretty_log() calls in production code PrettyLog.This.logMode = UNIT_TESTS_LOG_MODE; }); // ========================================================================== // MnemonicsToHDWalletInfo TESTS - BITCOIN // ========================================================================== describe('MnemonicsToHDWalletInfo - Bitcoin', () => { test('generates correct HD wallet info for Bitcoin with default parameters', async () => { const args = { [BLOCKCHAIN]: BITCOIN }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); // Check basic structure expect(result).toBeDefined(); expect(result[BLOCKCHAIN]).toBe(BITCOIN); expect(result[COIN]).toBe(COIN_ABBREVIATIONS[BITCOIN]); expect(result[COIN_TYPE]).toBe(COIN_TYPES[BITCOIN]); expect(result[MNEMONICS]).toBe(TEST_MNEMONICS); // Check key components are present expect(result[MASTER_PK_HEX]).toBeDefined(); expect(result[MASTER_PK_HEX]).toMatch(/^[0-9a-f]{64}$/i); expect(result[CHAINCODE]).toBeDefined(); expect(result[CHAINCODE]).toMatch(/^[0-9a-f]{64}$/i); expect(result[BIP32_ROOT_KEY]).toBeDefined(); expect(result[BIP32_ROOT_KEY]).toMatch(/^xprv/); // Check derived keys expect(result[PRIVATE_KEY]).toBeDefined(); expect(result[PRIVATE_KEY]).toMatch(/^[0-9a-f]{64}$/i); expect(result[PRIV_KEY]).toBeDefined(); expect(result[ADDRESS]).toBeDefined(); expect(typeof result[ADDRESS]).toBe('string'); expect(result[ADDRESS].length).toBeGreaterThan(0); // Check extended keys expect(result[ACCOUNT_XPRIV]).toBeDefined(); expect(result[ACCOUNT_XPRIV]).toMatch(/^xprv/); expect(result[ACCOUNT_XPUB]).toBeDefined(); expect(result[ACCOUNT_XPUB]).toMatch(/^xpub/); // Check WIF expect(result[WIF]).toBeDefined(); }); test('generates correct derivation path for Bitcoin', async () => { const args = { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: 0, [ADDRESS_INDEX]: 0 }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[DERIVATION_PATH]).toBeDefined(); expect(result[DERIVATION_PATH]).toBe("m/44'/0'/0'/0/0'"); }); test('respects custom BIP32 protocol parameter', async () => { const args = { [BLOCKCHAIN]: BITCOIN, [BIP32_PROTOCOL]: 49 // BIP49 for SegWit }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[DERIVATION_PATH]).toContain("m/49'/"); }); test('handles BIP32 passphrase correctly', async () => { const passphrase = "test passphrase"; const args = { [BLOCKCHAIN]: BITCOIN, [BIP32_PASSPHRASE]: passphrase }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[BIP32_PASSPHRASE]).toBe(passphrase); // Verify different result with passphrase const argsNoPass = { [BLOCKCHAIN]: BITCOIN }; const resultNoPass = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, argsNoPass); expect(result[ADDRESS]).not.toBe(resultNoPass[ADDRESS]); }); test('handles custom account index', async () => { const args = { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: 5, [ADDRESS_INDEX]: 0 }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[DERIVATION_PATH]).toContain("5'/0/0'"); }); test('handles custom address index', async () => { const args = { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: 0, [ADDRESS_INDEX]: 10 }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[DERIVATION_PATH]).toContain("0'/0/10'"); }); test('uses default mnemonics when none provided', async () => { const args = { [BLOCKCHAIN]: BITCOIN }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(undefined, args); expect(result[MNEMONICS]).toBe("much bottom such hurt hunt welcome cushion erosion pulse admit name deer"); }); }); // ========================================================================== // MnemonicsToHDWalletInfo TESTS - ETHEREUM // ========================================================================== describe('MnemonicsToHDWalletInfo - Ethereum', () => { test('generates correct HD wallet info for Ethereum', async () => { const args = { [BLOCKCHAIN]: ETHEREUM }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[BLOCKCHAIN]).toBe(ETHEREUM); expect(result[COIN]).toBe(COIN_ABBREVIATIONS[ETHEREUM]); expect(result[ADDRESS]).toBeDefined(); expect(typeof result[ADDRESS]).toBe('string'); expect(result[ADDRESS]).toMatch(/^0x[0-9a-fA-F]{40}$/); }); test('generates different addresses for different account indices', async () => { const args1 = { [BLOCKCHAIN]: ETHEREUM, [ACCOUNT]: 0 }; const args2 = { [BLOCKCHAIN]: ETHEREUM, [ACCOUNT]: 1 }; const result1 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args1); const result2 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args2); expect(result1[ADDRESS]).not.toBe(result2[ADDRESS]); expect(result1[PRIVATE_KEY]).not.toBe(result2[PRIVATE_KEY]); }); }); // ========================================================================== // MnemonicsToHDWalletInfo TESTS - OTHER BLOCKCHAINS // ========================================================================== describe('MnemonicsToHDWalletInfo - Multiple Blockchains', () => { const blockchains = [ DOGECOIN, LITECOIN, BITCOIN_CASH ]; test.each(blockchains)('generates valid wallet for %s', async (blockchain) => { const args = { [BLOCKCHAIN]: blockchain }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[BLOCKCHAIN]).toBe(blockchain); expect(result[COIN]).toBe(COIN_ABBREVIATIONS[blockchain]); expect(result[COIN_TYPE]).toBe(COIN_TYPES[blockchain]); expect(result[ADDRESS]).toBeDefined(); expect(result[PRIVATE_KEY]).toBeDefined(); expect(result[MASTER_PK_HEX]).toMatch(/^[0-9a-f]{64}$/i); }); }); // ========================================================================== // MnemonicsToHDWalletInfo TESTS - SPECIAL CASES // ========================================================================== describe('MnemonicsToHDWalletInfo - Special Cases', () => { test('handles Bitcoin Cash address conversion', async () => { const args = { [BLOCKCHAIN]: BITCOIN_CASH }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[ADDRESS]).toBeDefined(); // Bitcoin Cash addresses use cashaddr format expect(result[ADDRESS]).toMatch(/^(bitcoincash:|q)/); }); test('handles Stellar special case for ACCOUNT_XPRIV', async () => { const args = { [BLOCKCHAIN]: STELLAR }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); // For Stellar, ACCOUNT_XPRIV uses Stellar's secret key format (starts with 'S') // Not hex format if (result[ACCOUNT_XPRIV]) { expect(result[ACCOUNT_XPRIV]).toBeDefined(); expect(typeof result[ACCOUNT_XPRIV]).toBe('string'); expect(result[ACCOUNT_XPRIV]).toMatch(/^S[A-Z2-7]{55}$/); // Stellar secret key format } }); test('handles Stellar special case for WIF', async () => { const args = { [BLOCKCHAIN]: STELLAR }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[WIF]).toBeDefined(); // For Stellar, WIF is in Stellar's secret key format (starts with S) // Not hex format as originally expected expect(typeof result[WIF]).toBe('string'); expect(result[WIF].length).toBeGreaterThan(0); }); }); // ========================================================================== // GetDerivationPath TESTS // ========================================================================== describe('GetDerivationPath', () => { test('generates correct derivation path with default parameters', () => { const coinType = "0"; // Bitcoin const path = Bip32Utils.GetDerivationPath(coinType); expect(path).toBe("m/44'/0'/0'/0/0"); }); test('generates correct derivation path with custom account', () => { const coinType = "0"; const options = { [ACCOUNT]: 5 }; const path = Bip32Utils.GetDerivationPath(coinType, options); expect(path).toBe("m/44'/0'/5'/0/0"); }); test('generates correct derivation path with custom address index', () => { const coinType = "0"; const options = { [ADDRESS_INDEX]: 10 }; const path = Bip32Utils.GetDerivationPath(coinType, options); expect(path).toBe("m/44'/0'/0'/0/10"); }); test('generates correct derivation path with custom BIP32 protocol', () => { const coinType = "0"; const options = { [BIP32_PROTOCOL]: 49 }; const path = Bip32Utils.GetDerivationPath(coinType, options); expect(path).toBe("m/49'/0'/0'/0/0"); }); test('generates correct derivation path with all custom parameters', () => { const coinType = "60"; // Ethereum const options = { [BIP32_PROTOCOL]: 44, [ACCOUNT]: 2, [ADDRESS_INDEX]: 7 }; const path = Bip32Utils.GetDerivationPath(coinType, options); expect(path).toBe("m/44'/60'/2'/0/7"); }); test('handles string parameters correctly', () => { const coinType = "0"; const options = { [ACCOUNT]: "3", [ADDRESS_INDEX]: "5", [BIP32_PROTOCOL]: "44" }; const path = Bip32Utils.GetDerivationPath(coinType, options); expect(path).toBe("m/44'/0'/3'/0/5"); }); test('handles number parameters correctly', () => { const coinType = "0"; const options = { [ACCOUNT]: 3, [ADDRESS_INDEX]: 5, [BIP32_PROTOCOL]: 44 }; const path = Bip32Utils.GetDerivationPath(coinType, options); expect(path).toBe("m/44'/0'/3'/0/5"); }); test('generates path for different coin types', () => { // Test various coin types const paths = [ { coinType: "0", expected: "m/44'/0'/0'/0/0" }, // Bitcoin { coinType: "60", expected: "m/44'/60'/0'/0/0" }, // Ethereum { coinType: "2", expected: "m/44'/2'/0'/0/0" }, // Litecoin { coinType: "3", expected: "m/44'/3'/0'/0/0" }, // Dogecoin { coinType: "501", expected: "m/44'/501'/0'/0/0" } // Solana ]; paths.forEach(({ coinType, expected }) => { const path = Bip32Utils.GetDerivationPath(coinType); expect(path).toBe(expected); }); }); test('handles undefined options parameter', () => { const coinType = "0"; const path = Bip32Utils.GetDerivationPath(coinType, undefined); expect(path).toBe("m/44'/0'/0'/0/0"); }); test('handles empty options object', () => { const coinType = "0"; const path = Bip32Utils.GetDerivationPath(coinType, {}); expect(path).toBe("m/44'/0'/0'/0/0"); }); }); // ========================================================================== // CONSISTENCY AND DETERMINISM TESTS // ========================================================================== describe('Consistency and Determinism', () => { test('generates same wallet for same mnemonics and parameters', async () => { const args = { [BLOCKCHAIN]: BITCOIN }; const result1 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); const result2 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result1[ADDRESS]).toBe(result2[ADDRESS]); expect(result1[PRIVATE_KEY]).toBe(result2[PRIVATE_KEY]); expect(result1[MASTER_PK_HEX]).toBe(result2[MASTER_PK_HEX]); expect(result1[CHAINCODE]).toBe(result2[CHAINCODE]); }); test('generates different wallets for different mnemonics', async () => { const args = { [BLOCKCHAIN]: BITCOIN }; const result1 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); const result2 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS_12, args); expect(result1[ADDRESS]).not.toBe(result2[ADDRESS]); expect(result1[PRIVATE_KEY]).not.toBe(result2[PRIVATE_KEY]); expect(result1[MASTER_PK_HEX]).not.toBe(result2[MASTER_PK_HEX]); }); test('master private key and chaincode have correct lengths', async () => { const args = { [BLOCKCHAIN]: BITCOIN }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); // Both should be 32 bytes = 64 hex characters expect(result[MASTER_PK_HEX]).toHaveLength(64); expect(result[CHAINCODE]).toHaveLength(64); }); test('private key has correct length', async () => { const args = { [BLOCKCHAIN]: BITCOIN }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); // Private key should be 32 bytes = 64 hex characters expect(result[PRIVATE_KEY]).toHaveLength(64); }); }); // ========================================================================== // INTEGRATION TESTS WITH Bip39Utils // ========================================================================== describe('Integration with Bip39Utils', () => { test('uses Bip39Utils.GetArgs correctly', async () => { // Test with minimal args const minimalArgs = { [BLOCKCHAIN]: BITCOIN }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, minimalArgs); expect(result).toBeDefined(); expect(result[BLOCKCHAIN]).toBe(BITCOIN); }); test('works with Bip39Utils generated mnemonics', async () => { // Generate entropy and mnemonics using Bip39Utils const testEntropy = "a".repeat(64); // 256 bits const mnemonics = Bip39Utils.EntropyToMnemonics(testEntropy); const args = { [BLOCKCHAIN]: BITCOIN }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(mnemonics, args); expect(result).toBeDefined(); expect(result[MNEMONICS]).toBe(mnemonics); expect(result[ADDRESS]).toBeDefined(); }); }); // ========================================================================== // USE CASE - HD Wallet with account, address index, and BIP39 passphrase // ========================================================================== // Scenario: user sets mnemonics, then enters Account=2, Address Index=5, // and a BIP39 passphrase — verifies the full derivation chain. // ========================================================================== describe('Use Case - HD Wallet with account, address index, and BIP39 passphrase', () => { const USE_CASE_MNEMONICS = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; const USE_CASE_PASSPHRASE = "my secret passphrase"; const USE_CASE_ACCOUNT = 2; const USE_CASE_ADDR_INDEX = 5; test('Bitcoin: derivation path reflects account=2 and address_index=5', async () => { const result = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: USE_CASE_ACCOUNT, [ADDRESS_INDEX]: USE_CASE_ADDR_INDEX, [BIP32_PASSPHRASE]: USE_CASE_PASSPHRASE }); expect(result[DERIVATION_PATH]).toBe("m/44'/0'/2'/0/5'"); }); test('Bitcoin: address and private key are deterministic', async () => { const args = { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: USE_CASE_ACCOUNT, [ADDRESS_INDEX]: USE_CASE_ADDR_INDEX, [BIP32_PASSPHRASE]: USE_CASE_PASSPHRASE }; const result1 = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, args); const result2 = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, args); expect(result1[ADDRESS]).toBe(result2[ADDRESS]); expect(result1[PRIVATE_KEY]).toBe(result2[PRIVATE_KEY]); }); test('Bitcoin: passphrase changes the derived address (same account/index)', async () => { const withPass = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: USE_CASE_ACCOUNT, [ADDRESS_INDEX]: USE_CASE_ADDR_INDEX, [BIP32_PASSPHRASE]: USE_CASE_PASSPHRASE }); const noPass = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: USE_CASE_ACCOUNT, [ADDRESS_INDEX]: USE_CASE_ADDR_INDEX }); expect(withPass[ADDRESS]).not.toBe(noPass[ADDRESS]); expect(withPass[PRIVATE_KEY]).not.toBe(noPass[PRIVATE_KEY]); }); test('Bitcoin: known reference values for account=2, index=5, passphrase', async () => { const result = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: USE_CASE_ACCOUNT, [ADDRESS_INDEX]: USE_CASE_ADDR_INDEX, [BIP32_PASSPHRASE]: USE_CASE_PASSPHRASE }); expect(result[ADDRESS]).toBe("1BQQ4VjXtPGd3YEV45vuMkNKjo42pjLLUB"); expect(result[PRIVATE_KEY]).toBe("ca2dc38c852262d0ac5df670935b1917eb2e3749ccc6afff46fdbd8cf5f8ff0f"); expect(result[WIF]).toBe("L3ziiGbcFsFG8j6RaYoBuozsRcCELoehz7Fa9CQm2S35Jwphiju3"); }); test('Ethereum: derivation path reflects account=2 and address_index=5', async () => { const result = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, { [BLOCKCHAIN]: ETHEREUM, [ACCOUNT]: USE_CASE_ACCOUNT, [ADDRESS_INDEX]: USE_CASE_ADDR_INDEX, [BIP32_PASSPHRASE]: USE_CASE_PASSPHRASE }); expect(result[DERIVATION_PATH]).toBe("m/44'/60'/2'/0/5'"); }); test('Ethereum: known reference address for account=2, index=5, passphrase', async () => { const result = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, { [BLOCKCHAIN]: ETHEREUM, [ACCOUNT]: USE_CASE_ACCOUNT, [ADDRESS_INDEX]: USE_CASE_ADDR_INDEX, [BIP32_PASSPHRASE]: USE_CASE_PASSPHRASE }); expect(result[ADDRESS]).toBe("0x67C0ae27e79Ba1B6f58af5DCf3d893f7394ac0e5"); expect(result[PRIVATE_KEY]).toBe("cfe52101b07064fb5f04b24efe752399fea57024f0899212b82ba2b481faf71e"); expect(result[ADDRESS]).toMatch(/^0x[0-9a-fA-F]{40}$/); }); }); // ========================================================================== // EDGE CASES // ========================================================================== describe('Edge Cases', () => { test('handles very large account index', async () => { const args = { [BLOCKCHAIN]: BITCOIN, [ACCOUNT]: 999999 }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[DERIVATION_PATH]).toContain("999999'"); }); test('handles very large address index', async () => { const args = { [BLOCKCHAIN]: BITCOIN, [ADDRESS_INDEX]: 999999 }; const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args); expect(result[DERIVATION_PATH]).toContain("999999'"); }); test('handles empty passphrase (should be same as no passphrase)', async () => { const args1 = { [BLOCKCHAIN]: BITCOIN, [BIP32_PASSPHRASE]: "" }; const args2 = { [BLOCKCHAIN]: BITCOIN }; const result1 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args1); const result2 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args2); expect(result1[ADDRESS]).toBe(result2[ADDRESS]); }); test('handles null passphrase (should be same as no passphrase)', async () => { const args1 = { [BLOCKCHAIN]: BITCOIN, [BIP32_PASSPHRASE]: null }; const args2 = { [BLOCKCHAIN]: BITCOIN }; const result1 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args1); const result2 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args2); expect(result1[ADDRESS]).toBe(result2[ADDRESS]); }); }); });