/** * EncryptID Wallet Sync * * Bridges WalletStore (encrypted localStorage) ↔ VaultManager (encrypted server blob) * so wallet associations survive across devices. * * On login: * 1. Load vault from server * 2. Merge vault wallets into local WalletStore (new entries only) * 3. Merge local wallets into vault (in case this device has wallets the vault doesn't) * 4. Save vault if anything changed * * On wallet add/remove: * syncWalletsToVault() pushes current WalletStore state into the vault. */ import { getWalletStore, type SafeWalletEntry } from './wallet-store'; import { getVaultManager, type AccountVault } from './vault'; import { getKeyManager } from './key-derivation'; import type { DocCrypto } from '../../shared/local-first/crypto'; /** * After login + vault load, merge wallets bidirectionally between * the local WalletStore and the remote AccountVault. * * Requires PRF-derived encryption key (for WalletStore) and DocCrypto (for VaultManager). */ export async function syncWalletsOnLogin(docCrypto: DocCrypto): Promise { try { const km = getKeyManager(); if (!km.isInitialized()) return; const keys = await km.getKeys(); if (!keys.encryptionKey) return; const walletStore = getWalletStore(keys.encryptionKey); const vaultManager = getVaultManager(docCrypto); const vault = await vaultManager.get(); const localWallets = await walletStore.list(); const vaultWallets = vault.wallets || []; let changed = false; // 1. Restore vault wallets → local WalletStore (if not already present) for (const vw of vaultWallets) { const exists = localWallets.find( lw => lw.safeAddress.toLowerCase() === vw.safeAddress.toLowerCase() && lw.chainId === vw.chainId ); if (!exists) { await walletStore.add({ safeAddress: vw.safeAddress, chainId: vw.chainId, eoaAddress: vw.eoaAddress, label: vw.label, }); } } // 2. Push local wallets → vault (if not already present) const updatedLocal = await walletStore.list(); const updatedVaultWallets = [...vaultWallets]; for (const lw of updatedLocal) { const exists = updatedVaultWallets.find( vw => vw.safeAddress.toLowerCase() === lw.safeAddress.toLowerCase() && vw.chainId === lw.chainId ); if (!exists) { updatedVaultWallets.push({ id: lw.id, safeAddress: lw.safeAddress, chainId: lw.chainId, eoaAddress: lw.eoaAddress, label: lw.label, addedAt: lw.addedAt, }); changed = true; } } // 3. Also ensure the server-side wallet_address profile field is set // This is the fallback for mobile (no PRF) — the JWT carries it. if (keys.eoaAddress) { await setServerWalletAddress(keys.eoaAddress).catch(() => {}); } // 4. Save vault if anything changed if (changed) { await vaultManager.update({ wallets: updatedVaultWallets }); } } catch (err) { console.warn('Wallet sync failed:', err); } } /** * Push current WalletStore state into the vault. * Call this after any wallet add/update/remove operation. */ export async function syncWalletsToVault(docCrypto: DocCrypto): Promise { try { const km = getKeyManager(); if (!km.isInitialized()) return; const keys = await km.getKeys(); if (!keys.encryptionKey) return; const walletStore = getWalletStore(keys.encryptionKey); const vaultManager = getVaultManager(docCrypto); const localWallets = await walletStore.list(); const vaultWallets = localWallets.map(lw => ({ id: lw.id, safeAddress: lw.safeAddress, chainId: lw.chainId, eoaAddress: lw.eoaAddress, label: lw.label, addedAt: lw.addedAt, })); await vaultManager.update({ wallets: vaultWallets }); } catch (err) { console.warn('Wallet vault sync failed:', err); } } /** * Set the wallet address on the server user profile. * This ensures the JWT carries the wallet address for non-PRF devices (mobile). */ async function setServerWalletAddress(walletAddress: string): Promise { const token = localStorage.getItem('encryptid_token'); if (!token) return; await fetch('/encryptid/api/wallet-capability', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ walletAddress }), }); }