@aladas-org/cryptocalc
Version:
Cryptocurrency wallet generator
764 lines (618 loc) • 25.5 kB
JavaScript
/**
* ============================================================================
* Unit Tests - HD Wallet Generation (BIP32/BIP44)
* ============================================================================
* Tests the generation of Hierarchical Deterministic (HD) Wallets
* Location: www/js/crypto/HDWallet/hd_wallet.js
* Note: Console.log suppression is configured globally in setup.js
* ============================================================================
*/
// Import required modules
const { HDWallet } = require('@crypto/HDWallet/hd_wallet.js');
const { CryptoServices } = require('@crypto/crypto_services.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,
COIN, COIN_TYPE, MAINNET, TESTNET
} = require('@crypto/const_blockchains.js');
// Import wallet property constants
const {
ADDRESS, PRIVATE_KEY, PUBLIC_KEY_HEX, CRYPTO_NET, PRIV_KEY
} = require('@crypto/const_wallet.js');
// Import keyword constants
const {
BLOCKCHAIN, NULL_BLOCKCHAIN,
WALLET_MODE, HD_WALLET_TYPE,
MNEMONICS, UUID, WIF,
BIP32_PROTOCOL, BIP32_PASSPHRASE,
ACCOUNT, ADDRESS_INDEX, DERIVATION_PATH
} = require('@www/js/const_keywords.js');
describe('HD Wallet Generation (BIP32/BIP44)', () => {
// Test entropy and UUID
let testEntropy;
let testUuid;
beforeAll(() => {
// Disable console.log from pretty_log() calls in production code
PrettyLog.This.logMode = UNIT_TESTS_LOG_MODE;
// Use deterministic test entropy
testEntropy = CRYPTO_CONFIG.TEST_ENTROPY_256;
// Generate a test UUID
const cryptoServices = CryptoServices.This;
testUuid = cryptoServices.getUUID();
});
// ==========================================================================
// INITIALIZATION TESTS
// ==========================================================================
describe('HDWallet Initialization', () => {
test('InitializeWallet creates null wallet with correct structure', () => {
const nullWallet = HDWallet.InitializeWallet();
expect(nullWallet).toBeDefined();
expect(nullWallet[BLOCKCHAIN]).toBe(NULL_BLOCKCHAIN);
expect(nullWallet[CRYPTO_NET]).toBe('Null-NET');
expect(nullWallet[UUID]).toBe('Null-UUID');
expect(nullWallet[BIP32_PROTOCOL]).toBe(44);
expect(nullWallet[ADDRESS]).toBe('Null-ADDRESS');
expect(nullWallet[MNEMONICS]).toBe('Null-MNEMONICS');
});
});
// ==========================================================================
// ERROR HANDLING TESTS
// ==========================================================================
describe('Error Handling', () => {
test('throws error when entropy_hex is undefined', async () => {
const args = {
[]: BITCOIN,
[]: MAINNET
};
await expect(
HDWallet.GetWallet(undefined, testUuid, args)
).rejects.toThrow("HDWallet.GetWallet 'entropy_hex' NOT DEFINED");
});
test('throws error when entropy_hex is empty string', async () => {
const args = {
[]: BITCOIN,
[]: MAINNET
};
await expect(
HDWallet.GetWallet('', testUuid, args)
).rejects.toThrow("HDWallet.GetWallet 'entropy_hex' NOT DEFINED");
});
test('throws error when salt_uuid is undefined', async () => {
const args = {
[]: BITCOIN,
[]: MAINNET
};
await expect(
HDWallet.GetWallet(testEntropy, undefined, args)
).rejects.toThrow("HDWallet.GetWallet 'salt_uuid' NOT DEFINED");
});
test('throws error when salt_uuid is empty string', async () => {
const args = {
[]: BITCOIN,
[]: MAINNET
};
await expect(
HDWallet.GetWallet(testEntropy, '', args)
).rejects.toThrow("HDWallet.GetWallet 'salt_uuid' NOT DEFINED");
});
});
// ==========================================================================
// BITCOIN HD WALLET TESTS
// ==========================================================================
describe('Bitcoin HD Wallet (BIP44)', () => {
let wallet;
beforeAll(async () => {
const args = {
[]: BITCOIN,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
});
test('generates a valid Bitcoin HD wallet', () => {
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(BITCOIN);
expect(wallet[WALLET_MODE]).toBe(HD_WALLET_TYPE);
});
test('has valid Bitcoin address', () => {
expect(wallet[ADDRESS]).toBeDefined();
expect(wallet[ADDRESS]).toBeValidBitcoinAddress();
});
test('has valid private key', () => {
expect(wallet[PRIVATE_KEY]).toBeDefined();
expect(wallet[PRIVATE_KEY]).toBeValidHash(64);
});
test('has valid WIF', () => {
expect(wallet[WIF]).toBeDefined();
expect(wallet[WIF]).toBeValidWIF();
});
test('has valid mnemonic phrase', () => {
expect(wallet[MNEMONICS]).toBeDefined();
expect(wallet[MNEMONICS]).toBeValidMnemonic();
});
test('has correct derivation path', () => {
expect(wallet[DERIVATION_PATH]).toBeDefined();
// Note: The actual format includes a trailing apostrophe
expect(wallet[DERIVATION_PATH]).toMatch(/^m\/44'\/0'\/0'\/0\/0'$/);
});
test('has coin type information', () => {
expect(wallet[COIN]).toBe('BTC');
expect(wallet[COIN_TYPE]).toBe(0);
});
});
// ==========================================================================
// ETHEREUM HD WALLET TESTS
// ==========================================================================
describe('Ethereum HD Wallet (BIP44)', () => {
let wallet;
beforeAll(async () => {
const args = {
[]: ETHEREUM,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
});
test('generates a valid Ethereum HD wallet', () => {
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(ETHEREUM);
expect(wallet[WALLET_MODE]).toBe(HD_WALLET_TYPE);
});
test('has valid Ethereum address', () => {
expect(wallet[ADDRESS]).toBeDefined();
expect(wallet[ADDRESS]).toBeValidEthereumAddress();
});
test('has valid private key', () => {
expect(wallet[PRIVATE_KEY]).toBeDefined();
expect(wallet[PRIVATE_KEY]).toBeValidHash(64);
});
test('has valid mnemonic phrase', () => {
expect(wallet[MNEMONICS]).toBeDefined();
expect(wallet[MNEMONICS]).toBeValidMnemonic();
});
test('has correct derivation path', () => {
expect(wallet[DERIVATION_PATH]).toBeDefined();
// Note: The actual format includes a trailing apostrophe
expect(wallet[DERIVATION_PATH]).toMatch(/^m\/44'\/60'\/0'\/0\/0'$/);
});
test('has coin type information', () => {
expect(wallet[COIN]).toBe('ETH');
expect(wallet[COIN_TYPE]).toBe(60);
});
});
// ==========================================================================
// LITECOIN HD WALLET TESTS
// ==========================================================================
describe('Litecoin HD Wallet (BIP44)', () => {
let wallet;
beforeAll(async () => {
const args = {
[]: LITECOIN,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
});
test('generates a valid Litecoin HD wallet', () => {
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(LITECOIN);
});
test('has valid Litecoin address', () => {
expect(wallet[ADDRESS]).toBeDefined();
expect(wallet[ADDRESS]).toMatch(/^[LM3]/);
});
test('has valid private key', () => {
expect(wallet[PRIVATE_KEY]).toBeDefined();
expect(wallet[PRIVATE_KEY]).toBeValidHash(64);
});
test('has correct derivation path', () => {
expect(wallet[DERIVATION_PATH]).toBeDefined();
expect(wallet[DERIVATION_PATH]).toMatch(/^m\/44'\/2'\/0'\/0\/0'$/);
});
test('has coin type information', () => {
expect(wallet[COIN]).toBe('LTC');
expect(wallet[COIN_TYPE]).toBe(2);
});
});
// ==========================================================================
// DOGECOIN HD WALLET TESTS
// ==========================================================================
describe('Dogecoin HD Wallet (BIP44)', () => {
let wallet;
beforeAll(async () => {
const args = {
[]: DOGECOIN,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
});
test('generates a valid Dogecoin HD wallet', () => {
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(DOGECOIN);
});
test('has valid Dogecoin address', () => {
expect(wallet[ADDRESS]).toBeDefined();
expect(wallet[ADDRESS]).toMatch(/^D/);
});
test('has valid private key', () => {
expect(wallet[PRIVATE_KEY]).toBeDefined();
expect(wallet[PRIVATE_KEY]).toBeValidHash(64);
});
test('has correct derivation path', () => {
expect(wallet[DERIVATION_PATH]).toBeDefined();
expect(wallet[DERIVATION_PATH]).toMatch(/^m\/44'\/3'\/0'\/0\/0'$/);
});
test('has coin type information', () => {
expect(wallet[COIN]).toBe('DOGE');
expect(wallet[COIN_TYPE]).toBe(3);
});
});
// ==========================================================================
// AVALANCHE HD WALLET TESTS (Uses Ethereum derivation)
// ==========================================================================
describe('Avalanche HD Wallet (BIP44 - Ethereum compatible)', () => {
let wallet;
beforeAll(async () => {
const args = {
[]: AVALANCHE,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
});
test('generates a valid Avalanche HD wallet', () => {
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(AVALANCHE);
});
test('has valid Ethereum-compatible address', () => {
expect(wallet[ADDRESS]).toBeDefined();
expect(wallet[ADDRESS]).toBeValidEthereumAddress();
});
test('has valid private key', () => {
expect(wallet[PRIVATE_KEY]).toBeDefined();
expect(wallet[PRIVATE_KEY]).toBeValidHash(64);
});
test('uses Ethereum derivation path', () => {
expect(wallet[DERIVATION_PATH]).toBeDefined();
expect(wallet[DERIVATION_PATH]).toMatch(/^m\/44'\/60'\/0'\/0\/0'$/);
});
test('has coin abbreviation (ETH since it uses Ethereum API)', () => {
// Note: Avalanche uses Ethereum API internally, so COIN will be 'ETH'
// This is expected behavior
expect(wallet[COIN]).toBe('ETH');
});
});
// ==========================================================================
// POLYGON HD WALLET TESTS (Uses Ethereum derivation)
// ==========================================================================
describe('Polygon HD Wallet (BIP44 - Ethereum compatible)', () => {
let wallet;
beforeAll(async () => {
const args = {
[]: POLYGON,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
});
test('generates a valid Polygon HD wallet', () => {
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(POLYGON);
});
test('has valid Ethereum-compatible address', () => {
expect(wallet[ADDRESS]).toBeDefined();
expect(wallet[ADDRESS]).toBeValidEthereumAddress();
});
test('uses Ethereum derivation path', () => {
expect(wallet[DERIVATION_PATH]).toBeDefined();
expect(wallet[DERIVATION_PATH]).toMatch(/^m\/44'\/60'\/0'\/0\/0'$/);
});
test('has coin abbreviation (ETH since it uses Ethereum API)', () => {
// Note: Polygon uses Ethereum API internally, so COIN will be 'ETH'
// This is expected behavior
expect(wallet[COIN]).toBe('ETH');
});
});
// ==========================================================================
// SOLANA HD WALLET TESTS
// ==========================================================================
describe('Solana HD Wallet (BIP44)', () => {
let wallet;
beforeAll(async () => {
const args = {
[]: SOLANA,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
});
test('generates a valid Solana HD wallet', () => {
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(SOLANA);
});
test('has valid Solana address', () => {
expect(wallet[ADDRESS]).toBeDefined();
expect(wallet[ADDRESS]).toMatch(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/);
});
test('has valid private key (Base58 format for Solana)', () => {
expect(wallet[PRIVATE_KEY]).toBeDefined();
// Solana private key is in Base58 format, not hex
expect(wallet[PRIVATE_KEY].length).toBeGreaterThan(0);
});
test('has valid mnemonic phrase', () => {
expect(wallet[MNEMONICS]).toBeDefined();
expect(wallet[MNEMONICS]).toBeValidMnemonic();
});
});
// ==========================================================================
// CARDANO HD WALLET TESTS
// ==========================================================================
describe('Cardano HD Wallet (BIP44)', () => {
let wallet;
beforeAll(async () => {
const args = {
[]: CARDANO,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
});
test('generates a valid Cardano HD wallet', () => {
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(CARDANO);
});
test('has valid Cardano address', () => {
expect(wallet[ADDRESS]).toBeDefined();
expect(wallet[ADDRESS]).toMatch(/^addr1/);
});
test('has valid private key (Cardano extended format)', () => {
expect(wallet[PRIVATE_KEY]).toBeDefined();
// Cardano uses extended private key format (xprv...)
expect(wallet[PRIVATE_KEY].length).toBeGreaterThan(0);
});
test('has valid mnemonic phrase', () => {
expect(wallet[MNEMONICS]).toBeDefined();
expect(wallet[MNEMONICS]).toBeValidMnemonic();
});
});
// ==========================================================================
// DERIVATION PATH TESTS
// ==========================================================================
describe('Derivation Path Variations', () => {
test('generates different addresses for different address indices', async () => {
const args1 = {
[]: BITCOIN,
[]: MAINNET,
[]: 0,
[]: 0
};
const args2 = {
[]: BITCOIN,
[]: MAINNET,
[]: 0,
[]: 1
};
const wallet1 = await HDWallet.GetWallet(testEntropy, testUuid, args1);
const wallet2 = await HDWallet.GetWallet(testEntropy, testUuid, args2);
expect(wallet1[ADDRESS]).not.toBe(wallet2[ADDRESS]);
expect(wallet1[DERIVATION_PATH]).not.toBe(wallet2[DERIVATION_PATH]);
});
test('generates different addresses for different accounts', async () => {
const args1 = {
[]: BITCOIN,
[]: MAINNET,
[]: 0,
[]: 0
};
const args2 = {
[]: BITCOIN,
[]: MAINNET,
[]: 1,
[]: 0
};
const wallet1 = await HDWallet.GetWallet(testEntropy, testUuid, args1);
const wallet2 = await HDWallet.GetWallet(testEntropy, testUuid, args2);
expect(wallet1[ADDRESS]).not.toBe(wallet2[ADDRESS]);
expect(wallet1[DERIVATION_PATH]).toMatch(/^m\/44'\/0'\/0'\/0\/0'$/);
expect(wallet2[DERIVATION_PATH]).toMatch(/^m\/44'\/0'\/1'\/0\/0'$/);
});
test('BIP44 protocol generates valid addresses', async () => {
const argsBip44 = {
[]: BITCOIN,
[]: MAINNET,
[]: 44,
[]: 0,
[]: 0
};
const walletBip44 = await HDWallet.GetWallet(testEntropy, testUuid, argsBip44);
expect(walletBip44[ADDRESS]).toBeDefined();
expect(walletBip44[ADDRESS]).toBeValidBitcoinAddress();
expect(walletBip44[DERIVATION_PATH]).toMatch(/^m\/44'/);
});
});
// ==========================================================================
// BIP32 PASSPHRASE TESTS
// ==========================================================================
describe('BIP32 Passphrase Support', () => {
test('generates different addresses with different passphrases', async () => {
const args1 = {
[]: BITCOIN,
[]: MAINNET,
[]: '',
[]: 0,
[]: 0
};
const args2 = {
[]: BITCOIN,
[]: MAINNET,
[]: 'MySecretPassphrase123',
[]: 0,
[]: 0
};
const wallet1 = await HDWallet.GetWallet(testEntropy, testUuid, args1);
const wallet2 = await HDWallet.GetWallet(testEntropy, testUuid, args2);
expect(wallet1[ADDRESS]).not.toBe(wallet2[ADDRESS]);
expect(wallet1[PRIVATE_KEY]).not.toBe(wallet2[PRIVATE_KEY]);
});
test('wallet with passphrase stores passphrase field', async () => {
const passphrase = 'MyTestPassphrase';
const args = {
[]: BITCOIN,
[]: MAINNET,
[]: passphrase,
[]: 0,
[]: 0
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet[BIP32_PASSPHRASE]).toBe(passphrase);
});
test('wallet without passphrase does not store passphrase field', async () => {
const args = {
[]: BITCOIN,
[]: MAINNET,
[]: '',
[]: 0,
[]: 0
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet[BIP32_PASSPHRASE]).toBeUndefined();
});
});
// ==========================================================================
// DEFAULT VALUES TESTS
// ==========================================================================
describe('Default Values', () => {
test('uses default values when args specify blockchain and crypto_net', async () => {
const args = {
[]: BITCOIN,
[]: MAINNET
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(BITCOIN);
expect(wallet[CRYPTO_NET]).toBe(MAINNET);
expect(wallet[DERIVATION_PATH]).toMatch(/^m\/44'\/0'\/0'\/0\/0'$/); // Default BIP44, account 0, index 0
});
test('defaults to Bitcoin when no blockchain specified', async () => {
const args = {
[]: MAINNET
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet[BLOCKCHAIN]).toBe(BITCOIN);
});
test('defaults to BIP44 when no protocol specified', async () => {
const args = {
[]: BITCOIN,
[]: MAINNET
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet[DERIVATION_PATH]).toMatch(/^m\/44'/);
});
test('defaults to account 0 and address_index 0', async () => {
const args = {
[]: BITCOIN,
[]: MAINNET
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet[DERIVATION_PATH]).toMatch(/\/0'\/0\/0'$/);
});
});
// ==========================================================================
// CROSS-BLOCKCHAIN CONSISTENCY TESTS
// ==========================================================================
describe('Cross-Blockchain Consistency', () => {
test('same entropy generates same mnemonic for all blockchains', async () => {
const blockchains = [BITCOIN, ETHEREUM, LITECOIN, DOGECOIN];
const mnemonics = [];
for (const blockchain of blockchains) {
const args = {
[]: blockchain,
[]: MAINNET
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
mnemonics.push(wallet[MNEMONICS]);
}
// All mnemonics should be identical
expect(mnemonics[0]).toBe(mnemonics[1]);
expect(mnemonics[0]).toBe(mnemonics[2]);
expect(mnemonics[0]).toBe(mnemonics[3]);
});
test('same entropy generates different addresses for different blockchains', async () => {
const args1 = {
[]: BITCOIN,
[]: MAINNET
};
const args2 = {
[]: ETHEREUM,
[]: MAINNET
};
const btcWallet = await HDWallet.GetWallet(testEntropy, testUuid, args1);
const ethWallet = await HDWallet.GetWallet(testEntropy, testUuid, args2);
expect(btcWallet[ADDRESS]).not.toBe(ethWallet[ADDRESS]);
});
test('all HD wallets have HD_WALLET_TYPE mode', async () => {
const blockchains = [BITCOIN, ETHEREUM, LITECOIN, DOGECOIN];
for (const blockchain of blockchains) {
const args = {
[]: blockchain,
[]: MAINNET
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet[WALLET_MODE]).toBe(HD_WALLET_TYPE);
}
});
});
// ==========================================================================
// TESTNET TESTS
// ==========================================================================
describe('Testnet Support', () => {
test('generates Bitcoin testnet HD wallet', async () => {
const args = {
[]: BITCOIN,
[]: TESTNET,
[]: 0,
[]: 0
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(BITCOIN);
// Note: CRYPTO_NET might not be explicitly set in the wallet object for Bitcoin
expect(wallet[ADDRESS]).toBeValidBitcoinAddress();
});
test('generates Ethereum testnet HD wallet', async () => {
const args = {
[]: ETHEREUM,
[]: TESTNET,
[]: 0,
[]: 0
};
const wallet = await HDWallet.GetWallet(testEntropy, testUuid, args);
expect(wallet).toBeDefined();
expect(wallet[BLOCKCHAIN]).toBe(ETHEREUM);
expect(wallet[ADDRESS]).toBeValidEthereumAddress();
});
});
});