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>
173 lines
5.7 KiB
TypeScript
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')
|
|
);
|
|
});
|
|
});
|
|
});
|