import { generateUuid, uuidToBip32Words, publicKeyToBip32Words, bip32WordsToPublicKey, } from './bip32'; import { getDerivedPublicKeys, publicKeyBelongsToIdentity, } from './crypto'; import type { PairConfig } from '../types/identity'; const STORAGE_KEY_PAIRS = 'userwallet_pairs'; /** * Get stored pair configurations. */ export function getStoredPairs(): PairConfig[] { try { const stored = localStorage.getItem(STORAGE_KEY_PAIRS); if (stored === null) { return []; } return JSON.parse(stored) as PairConfig[]; } catch (error) { console.error('Error reading stored pairs:', error); return []; } } /** * Store pair configurations. */ export function storePairs(pairs: PairConfig[]): void { localStorage.setItem(STORAGE_KEY_PAIRS, JSON.stringify(pairs)); } /** * Create a new local pair. * Returns words encoding the identity public key (not the pair UUID). */ export function createLocalPair( membresParentsUuid: string[], identityPublicKey: string, ): { pair: PairConfig; words: string[]; } { const uuid = generateUuid(); const words = publicKeyToBip32Words(identityPublicKey); const pair: PairConfig = { uuid, membres_parents_uuid: membresParentsUuid, is_local: true, can_sign: true, publicKey: identityPublicKey, }; const pairs = getStoredPairs(); pairs.push(pair); storePairs(pairs); return { pair, words }; } /** * Add a remote pair from BIP32 words. * Words encode the remote identity public key (66 hex chars). */ export function addRemotePairFromWords( words: string[], membresParentsUuid: string[], ): PairConfig | null { const publicKey = bip32WordsToPublicKey(words); if (publicKey === null) { return null; } const uuid = generateUuid(); const pair: PairConfig = { uuid, membres_parents_uuid: membresParentsUuid, is_local: false, can_sign: false, publicKey, }; const pairs = getStoredPairs(); pairs.push(pair); storePairs(pairs); return pair; } /** * Generate BIP32 words for pairing without creating or storing a pair. * Use when showing QR/URL for second device; user adds remote pair on both * devices from these words. */ export function generatePairingWords(): string[] { const uuid = generateUuid(); return uuidToBip32Words(uuid); } const EXPECTED_PAIRING_WORD_COUNT = 17; // 17 words for public key (33 bytes) /** * Parse and validate pairing words from user input (whitespace-separated). * Returns word array if valid, null otherwise. */ export function parseAndValidatePairingWords(text: string): string[] | null { const words = text .toLowerCase() .split(/\s+/) .filter((w) => w.length > 0); if (words.length !== EXPECTED_PAIRING_WORD_COUNT) { return null; } const publicKey = bip32WordsToPublicKey(words); return publicKey !== null ? words : null; } /** * Check if pairing is satisfied (at least one pair available). */ export function isPairingSatisfied(): boolean { const pairs = getStoredPairs(); return pairs.length > 0; } /** * True if at least one stored pair is remote (is_local === false). */ export function hasRemotePair(): boolean { const pairs = getStoredPairs(); return pairs.some((p) => !p.is_local); } /** * Ensure a local pair exists for setup (1st or 2nd device). If none, create one. * Returns the local pair's words (encoding public key) for QR/URL. Idempotent. * Requires identity public key. */ export function ensureLocalPairForSetup(identityPublicKey: string): string[] { const pairs = getStoredPairs(); const local = pairs.find((p) => p.is_local); if (local !== undefined && local.publicKey !== undefined) { return publicKeyToBip32Words(local.publicKey); } const { words } = createLocalPair([], identityPublicKey); return words; } /** * Get pairs for a specific member. */ export function getPairsForMember(membreUuid: string): PairConfig[] { const pairs = getStoredPairs(); return pairs.filter((p) => p.membres_parents_uuid.includes(membreUuid)); } /** * Words for the local pair (encoding public key, BIP32-style). Null if no local pair or no public key. * Does not create a pair. */ export function getLocalPairWords(): string[] | null { const pairs = getStoredPairs(); const local = pairs.find((p) => p.is_local); if (local === undefined || local.publicKey === undefined) { return null; } return publicKeyToBip32Words(local.publicKey); } /** * Sync check not available; use usePairingConnected() hook for async result. * Kept for backward compatibility where only sync check was used. */ export function isPairingConnected(): boolean { return false; } /** * Remove a pair by UUID. */ export function removePair(pairUuid: string): void { const pairs = getStoredPairs(); const filtered = pairs.filter((p) => p.uuid !== pairUuid); storePairs(filtered); } /** * Update a pair's public key (for ECDH encryption). */ export function updatePairPublicKey(pairUuid: string, publicKey: string): void { const pairs = getStoredPairs(); const pair = pairs.find((p) => p.uuid === pairUuid); if (pair === undefined) { return; } pair.publicKey = publicKey; storePairs(pairs); } /** * Update a pair's persistent label (for display). */ export function updatePairLabel(pairUuid: string, label: string): void { const pairs = getStoredPairs(); const pair = pairs.find((p) => p.uuid === pairUuid); if (pair === undefined) { return; } pair.label = label; storePairs(pairs); } /** * Returns all public keys for a pair. For local pair, pass identity private key to include * derived keys (main + indices 0..derivedCount-1). For remote, returns publicKey + publicKeys. */ export function getPairPublicKeys( pair: PairConfig, identityPrivateKeyHex?: string, derivedCount: number = 0, ): string[] { if (pair.is_local && identityPrivateKeyHex !== undefined) { return getDerivedPublicKeys(identityPrivateKeyHex, derivedCount); } const keys: string[] = []; if (pair.publicKey !== undefined) { keys.push(pair.publicKey); } if (pair.publicKeys !== undefined) { for (const k of pair.publicKeys) { if (k !== pair.publicKey && !keys.includes(k)) { keys.push(k); } } } return keys; } /** * Returns true if the pair "owns" the given public key. Fast for local pair when identity * private key is passed (main key O(1), then at most maxDerived derivations). */ export function pairContainsPublicKey( pair: PairConfig, publicKeyHex: string, identityPrivateKeyHex?: string, maxDerived: number = 0, ): boolean { if (pair.is_local && identityPrivateKeyHex !== undefined) { return publicKeyBelongsToIdentity( identityPrivateKeyHex, publicKeyHex, maxDerived, ); } if (pair.publicKey === publicKeyHex) { return true; } return pair.publicKeys?.includes(publicKeyHex) ?? false; } /** * Add a derived public key to a pair (appends to publicKeys). Does not duplicate publicKey. */ export function addPairPublicKey(pairUuid: string, publicKeyHex: string): void { const pairs = getStoredPairs(); const pair = pairs.find((p) => p.uuid === pairUuid); if (pair === undefined) { return; } const existing = pair.publicKey === publicKeyHex || pair.publicKeys?.includes(publicKeyHex); if (existing) { return; } const next = pair.publicKeys ?? []; next.push(publicKeyHex); pair.publicKeys = next; storePairs(pairs); }