import { describe, it, expect } from 'vitest'; import { getPublicKey, nip19 } from 'nostr-tools'; import { generateMnemonicPhrase, validateMnemonicPhrase, privateKeyFromMnemonic, normalizeMnemonic, mnemonicToWords, wordsToMnemonic, deriveMultipleAccounts, } from './nip06'; describe('NIP-06 Key Derivation', () => { // Test vector from NIP-06 specification const TEST_MNEMONIC = 'leader monkey parrot ring guide accident before fence cannon height naive bean'; const TEST_PRIVATE_KEY_HEX = '7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a'; describe('generateMnemonicPhrase', () => { it('generates a valid 12-word mnemonic by default', () => { const mnemonic = generateMnemonicPhrase(); const words = mnemonic.split(' '); expect(words).toHaveLength(12); expect(validateMnemonicPhrase(mnemonic)).toBe(true); }); it('generates a valid 24-word mnemonic when requested', () => { const mnemonic = generateMnemonicPhrase(256); const words = mnemonic.split(' '); expect(words).toHaveLength(24); expect(validateMnemonicPhrase(mnemonic)).toBe(true); }); it('generates different mnemonics on each call', () => { const mnemonic1 = generateMnemonicPhrase(); const mnemonic2 = generateMnemonicPhrase(); expect(mnemonic1).not.toBe(mnemonic2); }); }); describe('validateMnemonicPhrase', () => { it('validates correct mnemonic', () => { expect(validateMnemonicPhrase(TEST_MNEMONIC)).toBe(true); }); it('rejects invalid mnemonic', () => { expect(validateMnemonicPhrase('invalid mnemonic phrase')).toBe(false); }); it('rejects empty string', () => { expect(validateMnemonicPhrase('')).toBe(false); }); it('rejects incorrect word count', () => { expect(validateMnemonicPhrase('word word word')).toBe(false); }); }); describe('privateKeyFromMnemonic', () => { it('derives correct private key from test vector', () => { const privateKey = privateKeyFromMnemonic(TEST_MNEMONIC); const privateKeyHex = Buffer.from(privateKey).toString('hex'); expect(privateKeyHex).toBe(TEST_PRIVATE_KEY_HEX); }); it('derives same key for same mnemonic', () => { const key1 = privateKeyFromMnemonic(TEST_MNEMONIC); const key2 = privateKeyFromMnemonic(TEST_MNEMONIC); expect(Buffer.from(key1).toString('hex')).toBe(Buffer.from(key2).toString('hex')); }); it('throws error for invalid mnemonic', () => { expect(() => { privateKeyFromMnemonic('invalid mnemonic'); }).toThrow('Invalid mnemonic phrase'); }); it('derives different keys for different accounts', () => { const key0 = privateKeyFromMnemonic(TEST_MNEMONIC, 0); const key1 = privateKeyFromMnemonic(TEST_MNEMONIC, 1); expect(Buffer.from(key0).toString('hex')).not.toBe(Buffer.from(key1).toString('hex')); }); it('can be encoded to valid nsec', () => { const privateKey = privateKeyFromMnemonic(TEST_MNEMONIC); const nsec = nip19.nsecEncode(privateKey); expect(nsec).toMatch(/^nsec1[a-z0-9]{58}$/); }); it('derives valid public key', () => { const privateKey = privateKeyFromMnemonic(TEST_MNEMONIC); const publicKey = getPublicKey(privateKey); expect(publicKey).toHaveLength(64); // Hex string of 32 bytes expect(publicKey).toMatch(/^[0-9a-f]{64}$/); }); }); describe('normalizeMnemonic', () => { it('trims whitespace', () => { expect(normalizeMnemonic(' test phrase ')).toBe('test phrase'); }); it('converts to lowercase', () => { expect(normalizeMnemonic('TEST PHRASE')).toBe('test phrase'); }); it('normalizes multiple spaces', () => { expect(normalizeMnemonic('test phrase here')).toBe('test phrase here'); }); it('handles mixed case and spacing', () => { expect(normalizeMnemonic(' TeSt PhRaSe ')).toBe('test phrase'); }); }); describe('mnemonicToWords / wordsToMnemonic', () => { it('splits mnemonic into word array', () => { const words = mnemonicToWords(TEST_MNEMONIC); expect(words).toHaveLength(12); expect(words[0]).toBe('leader'); expect(words[11]).toBe('bean'); }); it('joins word array into mnemonic', () => { const words = ['test', 'phrase', 'here']; const mnemonic = wordsToMnemonic(words); expect(mnemonic).toBe('test phrase here'); }); it('round-trips correctly', () => { const original = TEST_MNEMONIC; const words = mnemonicToWords(original); const reconstructed = wordsToMnemonic(words); expect(reconstructed).toBe(original); }); }); describe('deriveMultipleAccounts', () => { it('derives multiple accounts from one mnemonic', () => { const accounts = deriveMultipleAccounts(TEST_MNEMONIC, 3); expect(accounts).toHaveLength(3); expect(accounts[0]).toHaveLength(32); expect(accounts[1]).toHaveLength(32); expect(accounts[2]).toHaveLength(32); // All should be different const hex0 = Buffer.from(accounts[0]).toString('hex'); const hex1 = Buffer.from(accounts[1]).toString('hex'); const hex2 = Buffer.from(accounts[2]).toString('hex'); expect(hex0).not.toBe(hex1); expect(hex1).not.toBe(hex2); expect(hex0).not.toBe(hex2); }); it('first account matches single derivation', () => { const accounts = deriveMultipleAccounts(TEST_MNEMONIC, 3); const singleAccount = privateKeyFromMnemonic(TEST_MNEMONIC, 0); expect(Buffer.from(accounts[0]).toString('hex')).toBe( Buffer.from(singleAccount).toString('hex') ); }); }); });