@aladas-org/cryptocalc
Version:
Cryptocurrency wallet generator
669 lines (515 loc) • 24.2 kB
JavaScript
/**
* ============================================================================
* 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 = {
[]: 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 = {
[]: BITCOIN,
[]: 0,
[]: 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 = {
[]: BITCOIN,
[]: 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 = {
[]: BITCOIN,
[]: passphrase
};
const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args);
expect(result[BIP32_PASSPHRASE]).toBe(passphrase);
// Verify different result with passphrase
const argsNoPass = {
[]: BITCOIN
};
const resultNoPass = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, argsNoPass);
expect(result[ADDRESS]).not.toBe(resultNoPass[ADDRESS]);
});
test('handles custom account index', async () => {
const args = {
[]: BITCOIN,
[]: 5,
[]: 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 = {
[]: BITCOIN,
[]: 0,
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: ETHEREUM,
[]: 0
};
const args2 = {
[]: ETHEREUM,
[]: 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
};
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 = {
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: 44,
[]: 2,
[]: 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 = {
[]: "3",
[]: "5",
[]: "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 = {
[]: 3,
[]: 5,
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: 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 = {
[]: 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, {
[]: BITCOIN,
[]: USE_CASE_ACCOUNT,
[]: USE_CASE_ADDR_INDEX,
[]: 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 = {
[]: BITCOIN,
[]: USE_CASE_ACCOUNT,
[]: USE_CASE_ADDR_INDEX,
[]: 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, {
[]: BITCOIN,
[]: USE_CASE_ACCOUNT,
[]: USE_CASE_ADDR_INDEX,
[]: USE_CASE_PASSPHRASE
});
const noPass = await Bip32Utils.MnemonicsToHDWalletInfo(USE_CASE_MNEMONICS, {
[]: BITCOIN,
[]: USE_CASE_ACCOUNT,
[]: 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, {
[]: BITCOIN,
[]: USE_CASE_ACCOUNT,
[]: USE_CASE_ADDR_INDEX,
[]: 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, {
[]: ETHEREUM,
[]: USE_CASE_ACCOUNT,
[]: USE_CASE_ADDR_INDEX,
[]: 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, {
[]: ETHEREUM,
[]: USE_CASE_ACCOUNT,
[]: USE_CASE_ADDR_INDEX,
[]: 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 = {
[]: BITCOIN,
[]: 999999
};
const result = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args);
expect(result[DERIVATION_PATH]).toContain("999999'");
});
test('handles very large address index', async () => {
const args = {
[]: BITCOIN,
[]: 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 = {
[]: BITCOIN,
[]: ""
};
const args2 = {
[]: 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 = {
[]: BITCOIN,
[]: null
};
const args2 = {
[]: BITCOIN
};
const result1 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args1);
const result2 = await Bip32Utils.MnemonicsToHDWalletInfo(TEST_MNEMONICS, args2);
expect(result1[ADDRESS]).toBe(result2[ADDRESS]);
});
});
});