diff --git a/src/encryptid/index.ts b/src/encryptid/index.ts index 7992dee..923760c 100644 --- a/src/encryptid/index.ts +++ b/src/encryptid/index.ts @@ -78,6 +78,15 @@ export { type AccountVault, } from './vault'; +// ============================================================================ +// WALLET SYNC +// ============================================================================ + +export { + syncWalletsOnLogin, + syncWalletsToVault, +} from './wallet-sync'; + // ============================================================================ // UI COMPONENTS // ============================================================================ diff --git a/src/encryptid/ui/login-button.ts b/src/encryptid/ui/login-button.ts index 36e7975..3dd3ce7 100644 --- a/src/encryptid/ui/login-button.ts +++ b/src/encryptid/ui/login-button.ts @@ -16,6 +16,7 @@ import { getKeyManager } from '../key-derivation'; import { getSessionManager, AuthLevel } from '../session'; import { getDocBridge, resetDocBridge } from '../../../shared/local-first/encryptid-bridge'; import { getVaultManager, resetVaultManager } from '../vault'; +import { syncWalletsOnLogin } from '../wallet-sync'; // ============================================================================ // STYLES @@ -415,9 +416,9 @@ export class EncryptIDLoginButton extends HTMLElement { // Load encrypted account vault in background const docCrypto = getDocBridge().getDocCrypto(); if (docCrypto) { - getVaultManager(docCrypto).load().catch(err => - console.warn('Vault load failed:', err) - ); + getVaultManager(docCrypto).load() + .then(() => syncWalletsOnLogin(docCrypto)) + .catch(err => console.warn('Vault load failed:', err)); } } @@ -429,7 +430,7 @@ export class EncryptIDLoginButton extends HTMLElement { await sessionManager.createSession(result, keys.did, { encrypt: true, sign: true, - wallet: false, // Will be true after wallet setup + wallet: !!keys.eoaAddress, }); // Dispatch success event @@ -534,9 +535,9 @@ export class EncryptIDLoginButton extends HTMLElement { // Load encrypted account vault in background const docCrypto = getDocBridge().getDocCrypto(); if (docCrypto) { - getVaultManager(docCrypto).load().catch(err => - console.warn('Vault load failed:', err) - ); + getVaultManager(docCrypto).load() + .then(() => syncWalletsOnLogin(docCrypto)) + .catch(err => console.warn('Vault load failed:', err)); } } @@ -546,7 +547,7 @@ export class EncryptIDLoginButton extends HTMLElement { await sessionManager.createSession(result, keys.did, { encrypt: true, sign: true, - wallet: false, + wallet: !!keys.eoaAddress, }); this.dispatchEvent(new CustomEvent('login-success', { diff --git a/src/encryptid/wallet-sync.ts b/src/encryptid/wallet-sync.ts new file mode 100644 index 0000000..d5a8fe4 --- /dev/null +++ b/src/encryptid/wallet-sync.ts @@ -0,0 +1,145 @@ +/** + * 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 }), + }); +}