SkateSpots/src/lib/nip06.test.ts
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

173 lines
5.7 KiB
TypeScript

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')
);
});
});
});