/** * mavi vault in this browser only (localStorage). * Secrets are encrypted; never send vault contents to krypco servers. */ import { decryptVaultSecret, encryptVaultSecret, isVaultRecord, type CreatedWallet, type VaultRecord, walletFromMnemonic, } from "@krypco/mavi-core"; const VAULT_KEY = "mavi_vault_v1"; const SESSION_KEY = "mavi_unlocked_v1"; export type UnlockedMavi = { address: `0x${string}`; mnemonic: string; privateKey: `0x${string}`; wordCount: 12 | 24; }; export function readVault(): VaultRecord | null { if (typeof window === "undefined") return null; try { const raw = window.localStorage.getItem(VAULT_KEY); if (!raw) return null; const parsed: unknown = JSON.parse(raw); return isVaultRecord(parsed) ? parsed : null; } catch { return null; } } export function hasVault(): boolean { return readVault() !== null; } export async function saveNewVault( password: string, wallet: CreatedWallet, ): Promise { const { salt, iv, ciphertext } = await encryptVaultSecret(password, { mnemonic: wallet.mnemonic, }); const record: VaultRecord = { version: 1, brand: "mavi", address: wallet.address, wordCount: wallet.wordCount, createdAt: new Date().toISOString(), salt, iv, ciphertext, }; window.localStorage.setItem(VAULT_KEY, JSON.stringify(record)); return record; } export async function unlockVault(password: string): Promise { const record = readVault(); if (!record) throw new Error("No mavi wallet on this browser yet"); const secret = await decryptVaultSecret(password, record); const wallet = walletFromMnemonic(secret.mnemonic, record.wordCount); const unlocked: UnlockedMavi = { address: wallet.address, mnemonic: wallet.mnemonic, privateKey: wallet.privateKey, wordCount: wallet.wordCount, }; writeUnlockedSession(unlocked); return unlocked; } function writeUnlockedSession(unlocked: UnlockedMavi): void { // Session-only (this browser tab/window group): lock clears it. try { window.sessionStorage.setItem( SESSION_KEY, JSON.stringify({ address: unlocked.address, wordCount: unlocked.wordCount, mnemonic: unlocked.mnemonic, privateKey: unlocked.privateKey, unlockedAt: new Date().toISOString(), }), ); window.dispatchEvent(new Event("mavi-unlock")); } catch { /* private browsing quota */ } } /** True when this tab already has an unlocked session (no password again). */ export function isUnlocked(): boolean { return readUnlockedSession() !== null; } /** * Prefer existing unlocked session; otherwise unlock with password. * Use from Send / any gated action so one unlock covers the whole tab. */ export async function ensureUnlocked( password?: string, ): Promise { const existing = readUnlockedSession(); if (existing) return existing; if (!password) { throw new Error("Unlock mavi first (enter your password)."); } return unlockVault(password); } export function readUnlockedSession(): UnlockedMavi | null { if (typeof window === "undefined") return null; try { const raw = window.sessionStorage.getItem(SESSION_KEY); if (!raw) return null; const p = JSON.parse(raw) as Partial; if ( typeof p.address === "string" && typeof p.mnemonic === "string" && typeof p.privateKey === "string" && (p.wordCount === 12 || p.wordCount === 24) ) { return { address: p.address as `0x${string}`, mnemonic: p.mnemonic, privateKey: p.privateKey as `0x${string}`, wordCount: p.wordCount, }; } return null; } catch { return null; } } export function lockSession(): void { if (typeof window === "undefined") return; try { window.sessionStorage.removeItem(SESSION_KEY); } catch { /* ignore */ } } /** Danger: removes the encrypted vault from this browser. Recovery phrase still works if written down. */ export function deleteVaultLocal(): void { if (typeof window === "undefined") return; try { window.localStorage.removeItem(VAULT_KEY); } catch { /* ignore */ } lockSession(); } export function shortAddress(address: string): string { if (address.length < 12) return address; return `${address.slice(0, 6)}…${address.slice(-4)}`; }