import { initWebsocket, sendMessage } from '../websockets'; import { memoryManager } from './memory-manager'; import { secureLogger } from './secure-logger'; import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff, } from '../../pkg/sdk_client'; import ModalService from './modal.service'; import Database from './database.service'; import { storeData, retrieveData } from './storage.service'; import { BackUp } from '../models/backup.model'; import { DATABASE_CONFIG } from './database-config'; export const U32_MAX = 4294967295; const BASEURL = import.meta.env.VITE_BASEURL || `http://localhost`; const BOOTSTRAPURL = [import.meta.env.VITE_BOOTSTRAPURL || `${BASEURL}:8090`]; const STORAGEURL = import.meta.env.VITE_STORAGEURL || `${BASEURL}:8081`; const BLINDBITURL = import.meta.env.VITE_BLINDBITURL || `${BASEURL}:8000`; const DEFAULTAMOUNT = 1000n; // Global loading spinner functions removed - now using updateUserStatus instead // Helper function to update user status (can be called from static methods) function updateUserStatusHelper(message: string): void { try { const container = document.querySelector('login-4nk-component') as HTMLElement; const mainStatus = container?.querySelector('#main-status') as HTMLElement; if (mainStatus) { // Add timestamp for better user experience const timestamp = new Date().toLocaleTimeString(); mainStatus.innerHTML = `[${timestamp}] ${message}`; } } catch (error) { secureLogger.warn('Could not update user status', error as Error, { component: 'Service' }); } } const EMPTY32BYTES = String('').padStart(64, '0'); export default class Services { private static initializing: Promise | null = null; private static instance: Services; private processId: string | null = null; private stateId: string | null = null; private sdkClient: any; private processesCache: Record = {}; private myProcesses: Set = new Set(); private notifications: any[] | null = null; // private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; private maxCacheSize = 0; // Disabled caches completely private cacheExpiry = 0; // No cache expiry // private database: any; private routingInstance!: ModalService; private relayAddresses: { [wsurl: string]: string } = {}; private membersList: Record = {}; private currentBlockHeight: number = -1; private relayReadyResolver: (() => void) | null = null; private relayReadyPromise: Promise | null = null; private processedHandshakes: Set = new Set(); // Private constructor to prevent direct instantiation from outside private constructor() {} // Method to access the singleton instance of Services public static async getInstance(): Promise { if (Services.instance) { return Services.instance; } if (!Services.initializing) { Services.initializing = (async () => { const instance = new Services(); // Initialize WebAssembly when needed await instance.init(); instance.routingInstance = await ModalService.getInstance(); return instance; })(); } secureLogger.info('Initializing services', { component: 'Service' }); // Debug: Check memory usage before any operations if ((performance as any).memory) { const memory = (performance as any).memory; const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100; secureLogger.debug(`Initial memory usage: ${usedPercent.toFixed(1)}% (${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB / ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(1)}MB)`, { component: 'Service' }); // Si la mémoire est déjà très élevée, faire un nettoyage agressif immédiat if (usedPercent > 90) { secureLogger.warn('High memory detected, performing immediate cleanup...', { component: 'Service' }); // Nettoyage agressif immédiat if (window.gc) { for (let i = 0; i < 5; i++) { window.gc(); } } // Nettoyer les caches if ('caches' in window) { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map(name => caches.delete(name))); } // Nettoyer localStorage if (window.localStorage) { const keys = Object.keys(localStorage); keys.forEach(key => { if (key.startsWith('temp_') || key.startsWith('cache_') || key.startsWith('vite_')) { localStorage.removeItem(key); } }); } // Vérifier la mémoire après nettoyage const memoryAfter = (performance as any).memory; const usedPercentAfter = (memoryAfter.usedJSHeapSize / memoryAfter.jsHeapSizeLimit) * 100; secureLogger.debug(`Memory after cleanup: ${usedPercentAfter.toFixed(1)}% (${(memoryAfter.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB)`, { component: 'Service' }); } } // Update user status during initialization (using helper function) updateUserStatusHelper('🔄 Initializing services...'); // Add WebAssembly memory optimization and error handling try { // Check if WebAssembly is supported if (typeof WebAssembly === 'undefined') { throw new Error('WebAssembly is not supported in this browser'); } // Optimize WebAssembly memory before initialization secureLogger.info('Optimizing WebAssembly memory...', { component: 'Service' }); // Clear browser caches to free memory if ('caches' in window) { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map(name => caches.delete(name))); secureLogger.debug('Browser caches cleared', { component: 'Service' }); } // Clear unused objects from memory if (window.gc) { window.gc(); secureLogger.debug('Garbage collection triggered', { component: 'Service' }); } // Force memory cleanup if (window.gc) { window.gc(); await new Promise(resolve => setTimeout(resolve, 100)); // Wait for GC window.gc(); secureLogger.debug('Additional garbage collection triggered', { component: 'Service' }); } // DO NOT clear user data - only clear non-essential caches secureLogger.info('Skipping storage cleanup to preserve user data', { component: 'Service' }); // Light memory cleanup only secureLogger.info('Performing light memory cleanup...', { component: 'Service' }); // Minimal cleanup to avoid memory leaks try { // Only clear HTTP caches if they exist if ('caches' in window) { const cacheNames = await caches.keys(); if (cacheNames.length > 0) { const httpCaches = cacheNames.filter(name => name.startsWith('http')); if (httpCaches.length > 0) { await Promise.all(httpCaches.map(name => caches.delete(name))); secureLogger.debug('HTTP caches cleared (user data preserved)', { component: 'Service' }); } } } } catch (e) { secureLogger.warn('Safe cleanup error', e as Error, { component: 'Service' }); } // Check available memory (Chrome-specific API) if ((performance as any).memory) { const memory = (performance as any).memory; const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100; secureLogger.info('📊 Memory usage after cleanup: ${usedPercent.toFixed(1)}% (${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB)', { component: 'Service' }); if (usedPercent > 75) { secureLogger.warn('⚠️ High memory usage detected, performing aggressive cleanup...', { component: 'Service' }); // More aggressive cleanup secureLogger.debug('🔍 Debugging memory usage...', { component: 'Service' }); secureLogger.info('📦 Document elements:', { component: 'Service', data: document.querySelectorAll('*' }).length); // Multiple garbage collections if (window.gc) { for (let i = 0; i < 3; i++) { window.gc(); await new Promise(resolve => setTimeout(resolve, 100)); } } // Clear any cached data if (window.localStorage) { const keys = Object.keys(localStorage); keys.forEach(key => { if (key.startsWith('temp_') || key.startsWith('cache_')) { localStorage.removeItem(key); } }); } secureLogger.info('🧹 Aggressive memory cleanup completed', { component: 'Service' }); } } } catch (error) { secureLogger.error('❌ WebAssembly optimization error:', error, { component: 'Service' }); // Don't throw here, continue with initialization } // Initialize services with conditional WebAssembly loading try { // Check memory before loading WebAssembly if ((performance as any).memory) { const memory = (performance as any).memory; const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100; const availableMB = (memory.jsHeapSizeLimit - memory.usedJSHeapSize) / 1024 / 1024; secureLogger.info('📊 Memory check before WebAssembly: ${usedPercent.toFixed(1)}% used, ${availableMB.toFixed(1)}MB available', { component: 'Service' }); // WebAssembly nécessite généralement au moins 100-200MB de mémoire disponible // Si moins de 150MB disponibles ou plus de 85% utilisé, ne pas initialiser if (usedPercent > 85 || availableMB < 150) { secureLogger.error('🚫 Memory insufficient for WebAssembly: ${usedPercent.toFixed(1)}% used, ${availableMB.toFixed(1)}MB available', { component: 'Service' }); Services.initializing = null; throw new Error(`Insufficient memory for WebAssembly initialization. Current usage: ${usedPercent.toFixed(1)}%, Available: ${availableMB.toFixed(1)}MB. Please close other tabs and refresh.`); } } // Memory is sufficient, load WebAssembly Services.instance = await Services.initializing; Services.initializing = null; secureLogger.info('✅ Services initialized with WebAssembly', { component: 'Service' }); } catch (error) { secureLogger.error('❌ Service initialization failed:', error, { component: 'Service' }); // Réinitialiser initializing pour permettre une nouvelle tentative après un délai Services.initializing = null; // Si c'est une erreur de mémoire, ne pas réessayer immédiatement const errorMessage = (error as Error).message || String(error); if (errorMessage.includes('Out of memory') || errorMessage.includes('memory')) { secureLogger.error('🚫 Memory error detected - cannot retry immediately', { component: 'Service' }); throw new Error('WebAssembly initialization failed due to insufficient memory. Please refresh the page.'); } throw error; } // Update user status after successful initialization updateUserStatusHelper('✅ Services initialized successfully'); return Services.instance; } public async init(): Promise { this.notifications = this.getNotifications(); // Vérifier la mémoire avant d'importer WebAssembly if ((performance as any).memory) { const memory = (performance as any).memory; const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100; const availableMB = (memory.jsHeapSizeLimit - memory.usedJSHeapSize) / 1024 / 1024; secureLogger.info('📊 Memory check before WebAssembly import: ${usedPercent.toFixed(1)}% used, ${availableMB.toFixed(1)}MB available', { component: 'Service' }); // WebAssembly nécessite au moins 150MB de mémoire disponible if (usedPercent > 85 || availableMB < 150) { secureLogger.error('🚫 Memory insufficient for WebAssembly import: ${usedPercent.toFixed(1)}% used, ${availableMB.toFixed(1)}MB available', { component: 'Service' }); throw new Error(`Insufficient memory for WebAssembly. Current usage: ${usedPercent.toFixed(1)}%, Available: ${availableMB.toFixed(1)}MB. Please close other tabs and refresh.`); } } this.sdkClient = await import('../../pkg/sdk_client'); this.sdkClient.setup(); for (const wsurl of Object.values(BOOTSTRAPURL)) { this.updateRelay(wsurl, ''); } // Démarrer le monitoring de la mémoire memoryManager.startMonitoring(); // Nettoyer les caches périodiquement this.startCacheCleanup(); // Initialiser le service PBKDF2 pour les credentials sécurisés try { const { secureCredentialsService } = await import('./secure-credentials.service'); // Use secureCredentialsService variable secureLogger.info('Secure credentials service imported:', { component: 'Service', data: secureCredentialsService }); secureLogger.info('PBKDF2 service initialized for secure credentials', { component: 'Services', operation: 'pbkdf2_init' }); } catch (error) { secureLogger.warn('Failed to initialize PBKDF2 service', { component: 'Services', operation: 'pbkdf2_init', error: error as Error }); } secureLogger.info('Services initialized', { component: 'Services', operation: 'initialization' }); } public setProcessId(processId: string | null) { this.processId = processId; } /** * Démarre le nettoyage périodique des caches */ private startCacheCleanup(): void { setInterval(() => { this.cleanupCaches(); }, this.cacheExpiry); } /** * Nettoie les caches expirés */ private cleanupCaches(): void { const now = Date.now(); const expiredKeys: string[] = []; // Nettoyer le cache des processus Object.keys(this.processesCache).forEach(key => { const process = this.processesCache[key]; if (process && now - (process as any).timestamp > this.cacheExpiry) { expiredKeys.push(key); } }); expiredKeys.forEach(key => { delete this.processesCache[key]; }); // Nettoyer le cache des membres Object.keys(this.membersList).forEach(key => { const member = this.membersList[key]; if (member && now - (member as any).timestamp > this.cacheExpiry) { delete this.membersList[key]; } }); if (expiredKeys.length > 0) { secureLogger.debug('Cache cleanup completed', { component: 'Services', operation: 'cache_cleanup', expiredEntries: expiredKeys.length }); } } /** * Met en cache un processus avec timestamp */ private cacheProcess(processId: string, process: Process): void { // Use parameters secureLogger.info('Caching process:', { component: 'Service', data: { processId, process } }); if (Object.keys(this.processesCache).length >= this.maxCacheSize) { // Supprimer le plus ancien const oldestKey = Object.keys(this.processesCache)[0]; delete this.processesCache[oldestKey]; } (process as any).timestamp = Date.now(); this.processesCache[processId] = process; } /** * Récupère un processus du cache */ private getCachedProcess(processId: string): Process | null { // Use processId parameter secureLogger.info('Getting cached process:', { component: 'Service', data: processId }); const process = this.processesCache[processId]; if (!process) {return null;} const now = Date.now(); if (now - (process as any).timestamp > this.cacheExpiry) { delete this.processesCache[processId]; return null; } return process; } /** * Nettoie tous les caches */ public clearAllCaches(): void { this.processesCache = {}; this.membersList = {}; this.myProcesses.clear(); secureLogger.info('All caches cleared', { component: 'Services', operation: 'cache_clear' }); } /** * Récupère les statistiques des caches */ public getCacheStats(): { processes: number; members: number; myProcesses: number; memory: any; } { return { processes: Object.keys(this.processesCache).length, members: Object.keys(this.membersList).length, myProcesses: this.myProcesses.size, memory: memoryManager.getMemoryReport() }; } public setStateId(stateId: string | null) { this.stateId = stateId; } public getProcessId(): string | null { return this.processId; } public getStateId(): string | null { return this.stateId; } /** * Calls `this.addWebsocketConnection` for each `wsurl` in relayAddresses. * Waits for at least one handshake message before returning. */ public async connectAllRelays(): Promise { const relayUrls = Object.keys(this.relayAddresses); secureLogger.info('🚀 Connecting to ${relayUrls.length} relays in parallel...', { component: 'Service' }); // Create the relay ready promise immediately when starting connections this.getRelayReadyPromise(); // Connect to all relays in parallel const connectionPromises = relayUrls.map(async wsurl => { try { secureLogger.info('🔗 Connecting to: ${wsurl}', { component: 'Service' }); await this.addWebsocketConnection(wsurl); secureLogger.info('✅ Successfully connected to: ${wsurl}', { component: 'Service' }); return wsurl; } catch (error) { secureLogger.error('❌ Failed to connect to ${wsurl}:', error, { component: 'Service' }); return null; } }); // Wait for all connections to complete (success or failure) const results = await Promise.allSettled(connectionPromises); const connectedUrls = results .filter( (result): result is PromiseFulfilledResult => result.status === 'fulfilled' && result.value !== null ) .map(result => result.value); secureLogger.info('✅ Connected to ${connectedUrls.length}/${relayUrls.length} relays', { component: 'Service' }); // Wait for at least one handshake message if we have connections if (connectedUrls.length > 0) { try { await this.waitForHandshakeMessage(10000); // Augmenter le timeout à 10 secondes secureLogger.info('✅ Handshake received from at least one relay', { component: 'Service' }); } catch (error) { secureLogger.warn('⚠️ No handshake received within timeout, but continuing with ${connectedUrls.length} connections', { component: 'Service' }); // Continue anyway - we have connections even without handshake // Resolve the relay ready promise manually since we have connections this.resolveRelayReady(); } } else { secureLogger.warn('⚠️ No relay connections established', { component: 'Service' }); } } private getRelayReadyPromise(): Promise { secureLogger.debug('🔍 DEBUG: getRelayReadyPromise called, promise exists:', { component: 'Service', data: !!this.relayReadyPromise }); // If we already have a relay with spAddress, return resolved promise const hasRelayWithAddress = Object.values(this.relayAddresses).some(address => address && address.trim() !== ''); if (hasRelayWithAddress) { secureLogger.debug('🔍 DEBUG: Relay already ready with spAddress, returning resolved promise', { component: 'Service' }); return Promise.resolve(); } if (!this.relayReadyPromise) { secureLogger.debug('🔍 DEBUG: Creating new relay ready promise', { component: 'Service' }); this.relayReadyPromise = new Promise((resolve) => { this.relayReadyResolver = resolve; // Timeout après 10 secondes si aucun handshake n'arrive setTimeout(() => { if (this.relayReadyResolver) { secureLogger.warn('⚠️ Relay ready timeout - resolving anyway', { component: 'Service' }); this.relayReadyResolver(); this.relayReadyResolver = null; this.relayReadyPromise = null; } }, 10000); }); } else { secureLogger.debug('🔍 DEBUG: Returning existing relay ready promise', { component: 'Service' }); } return this.relayReadyPromise; } private resolveRelayReady(): void { secureLogger.debug('🔍 DEBUG: resolveRelayReady called, resolver exists:', { component: 'Service', data: !!this.relayReadyResolver }); if (this.relayReadyResolver) { secureLogger.debug('✅ DEBUG: Resolving relay ready promise', { component: 'Service' }); this.relayReadyResolver(); this.relayReadyResolver = null; this.relayReadyPromise = null; } else { secureLogger.warn('⚠️ DEBUG: No resolver to resolve - promise may have been resolved already or never created', { component: 'Service' }); } } public async addWebsocketConnection(url: string): Promise { secureLogger.info('Opening new websocket connection', { component: 'Service' }); await initWebsocket(url); } /** * Add or update a key/value pair in relayAddresses. * @param wsurl - The WebSocket URL (key). * @param spAddress - The SP Address (value). */ public updateRelay(url: string, spAddress: string) { secureLogger.info('✅ Updating relay ${url} with spAddress ${spAddress}', { component: 'Service' }); this.relayAddresses[url] = spAddress; } /** * Retrieve the spAddress for a given wsurl. * @param wsurl - The WebSocket URL to look up. * @returns The SP Address if found, or undefined if not. */ public getSpAddress(wsurl: string): string | undefined { return this.relayAddresses[wsurl]; } /** * Get all key/value pairs from relayAddresses. * @returns An array of objects containing wsurl and spAddress. */ public getAllRelays(): { wsurl: string; spAddress: string }[] { return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({ wsurl, spAddress, })); } /** * Print all key/value pairs for debugging. */ public printAllRelays(): void { secureLogger.info('Current relay addresses:', { component: 'Service' }); for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) { secureLogger.info('${wsurl} -> ${spAddress}', { component: 'Service' }); } } public isPaired(): boolean { try { if (!this.sdkClient) { secureLogger.info('WebAssembly SDK not initialized - assuming not paired', { component: 'Service' }); return false; } return this.sdkClient.is_paired(); } catch (e) { // During pairing process, it's normal for the device to not be paired yet secureLogger.warn('Device pairing status check failed (normal during pairing): ${e}', { component: 'Service' }); return false; } } public async unpairDevice(): Promise { try { this.sdkClient.unpair_device(); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); } catch (e) { throw new Error(`Failed to unpair device: ${e}`); } } public async getSecretForAddress(address: string): Promise { const db = await Database.getInstance(); return await db.getObject('shared_secrets', address); } public async getAllSecrets(): Promise { const db = await Database.getInstance(); const sharedSecrets = await db.dumpStore('shared_secrets'); const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); // keys are numeric values const secretsStore = { shared_secrets: sharedSecrets, unconfirmed_secrets: Object.values(unconfirmedSecrets), }; return secretsStore; } public async getAllDiffs(): Promise> { const db = await Database.getInstance(); return await db.dumpStore('diffs'); } public async getDiffByValue(value: string): Promise { const db = await Database.getInstance(); const store = 'diffs'; const res = await db.getObject(store, value); return res; } public async getTokensFromFaucet(): Promise { await this.ensureSufficientAmount(); } // If we're updating a process, we must call that after update especially if roles are part of it // We will take the roles from the last state, wheter it's commited or not public async checkConnections(process: Process, stateId: string | null = null): Promise { if (process.states.length < 2) { throw new Error("Process doesn't have any state yet"); } let roles: Record | null = null; if (!stateId) { roles = process.states[process.states.length - 2].roles; } else { roles = process.states.find(state => state.state_id === stateId)?.roles || null; } if (!roles) { throw new Error('No roles found'); } const members: Set = new Set(); for (const role of Object.values(roles!)) { for (const member of role.members) { // Check if we know the member that matches this id const memberAddresses = this.getAddressesForMemberId(member); if (memberAddresses && memberAddresses.length != 0) { members.add({ sp_addresses: memberAddresses }); } } } if (members.size === 0) { // This must be a pairing process // Check if we have a pairedAddresses in the public data let publicData: Record | null = null; if (!stateId) { publicData = process.states[process.states.length - 2]?.public_data; } else { publicData = process.states.find(state => state.state_id === stateId)?.public_data || null; } // If pairedAddresses is not in the current state, look in previous states if (!publicData?.['pairedAddresses']) { // Look for pairedAddresses in previous states for (let i = process.states.length - 1; i >= 0; i--) { const state = process.states[i]; if (state.public_data && state.public_data['pairedAddresses']) { publicData = state.public_data; break; } } } if (!publicData?.['pairedAddresses']) { throw new Error('Not a pairing process'); } const decodedAddresses = this.decodeValue(publicData['pairedAddresses']); if (decodedAddresses.length === 0) { throw new Error('Not a pairing process'); } members.add({ sp_addresses: decodedAddresses }); } // Ensure the amount is available before proceeding await this.getTokensFromFaucet(); const unconnectedAddresses = new Set(); const myAddress = await this.getDeviceAddress(); for (const member of Array.from(members)) { const sp_addresses = member.sp_addresses; if (!sp_addresses || sp_addresses.length === 0) {continue;} for (const address of sp_addresses) { // For now, we ignore our own device address, although there might be use cases for having a secret with ourselves if (address === myAddress) {continue;} if ((await this.getSecretForAddress(address)) === null) { unconnectedAddresses.add(address); } } } if (unconnectedAddresses && unconnectedAddresses.size != 0) { const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses)); await this.handleApiReturn(apiResult); } } public async connectAddresses(addresses: string[]): Promise { if (addresses.length === 0) { throw new Error('Trying to connect to empty addresses list'); } try { return this.sdkClient.create_transaction(addresses, 1); } catch (e) { secureLogger.error('Failed to connect member:', e, { component: 'Service' }); throw e; } } private async ensureSufficientAmount(): Promise { const availableAmt = await this.getAmount(); const target: bigint = DEFAULTAMOUNT * 10n; secureLogger.info('💰 Current amount: ${availableAmt}, target: ${target}', { component: 'Service' }); if (availableAmt < target) { secureLogger.info('🪙 Requesting tokens from faucet...', { component: 'Service' }); this.updateUserStatus('🪙 Requesting test tokens from faucet...'); const faucetMsg = this.createFaucetMessage(); secureLogger.info('🪙 Faucet message created:', { component: 'Service', data: faucetMsg }); this.sendFaucetMessage(faucetMsg); secureLogger.info('🪙 Faucet message sent, waiting for tokens...', { component: 'Service' }); this.updateUserStatus('⏳ Waiting for tokens to be sent...'); await this.waitForAmount(target); } else { secureLogger.info('✅ Sufficient tokens already available', { component: 'Service' }); this.updateUserStatus('✅ Sufficient tokens available'); } } private isScanningBlocks = false; private hasReceivedTransaction = false; // Track if we've received any transaction private async safeScanBlocks(): Promise { if (this.isScanningBlocks) { secureLogger.warn('⏳ Block scan already in progress, skipping...', { component: 'Service' }); return; } this.isScanningBlocks = true; try { secureLogger.info('🔄 Starting block scan...', { component: 'Service' }); await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL); secureLogger.info('✅ Block scan completed', { component: 'Service' }); } catch (error) { secureLogger.error('❌ Block scan failed:', error, { component: 'Service' }); throw error; } finally { this.isScanningBlocks = false; } } private updateUserStatus(message: string): void { try { const container = document.querySelector('login-4nk-component') as HTMLElement; const mainStatus = container?.querySelector('#main-status') as HTMLElement; if (mainStatus) { // Add timestamp for better user experience const timestamp = new Date().toLocaleTimeString(); mainStatus.innerHTML = `[${timestamp}] ${message}`; } } catch (error) { secureLogger.warn('Could not update user status:', { component: 'Service', data: error }); } } private async waitForAmount(target: bigint): Promise { let attempts = 20; // Increased attempts for blockchain confirmation while (attempts > 0) { const amount = await this.getAmount(); secureLogger.info('🪙 Attempt ${21 - attempts}: current amount ${amount}, target ${target}', { component: 'Service' }); if (amount >= target) { secureLogger.info('✅ Sufficient tokens received!', { component: 'Service' }); this.updateUserStatus('✅ Tokens received successfully!'); return amount; } // Only scan if we've received a transaction (NewTx message) if (this.hasReceivedTransaction && attempts < 20) { secureLogger.info('🔄 Transaction received, scanning blocks to update wallet state...', { component: 'Service' }); this.updateUserStatus('🔄 Processing received transaction...'); try { // Force a complete scan to catch the new transaction secureLogger.info('🔄 Scanning blocks to catch new transaction...', { component: 'Service' }); await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL); secureLogger.info('✅ Block scan completed after transaction', { component: 'Service' }); // Check amount again after scanning const newAmount = await this.getAmount(); secureLogger.info('💰 Amount after transaction scan: ${newAmount}', { component: 'Service' }); if (newAmount > 0n) { this.updateUserStatus(`💰 Found ${newAmount} tokens in wallet!`); } else { this.updateUserStatus('⏳ Transaction processed, waiting for confirmation...'); } } catch (scanError) { secureLogger.error('❌ Error during transaction scan:', scanError, { component: 'Service' }); this.updateUserStatus('⚠️ Processing transaction...'); } } else if (!this.hasReceivedTransaction) { this.updateUserStatus('⏳ Waiting for faucet transaction...'); } attempts--; if (attempts > 0) { secureLogger.info('⏳ Waiting 5 seconds before next attempt (${attempts} attempts left)...', { component: 'Service' }); this.updateUserStatus(`⏳ Checking for tokens... (${attempts} attempts remaining)`); await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds } } throw new Error('Amount is still insufficient after 20 attempts - faucet may be down or transaction not confirmed'); } public async createPairingProcess(userName: string, pairWith: string[]): Promise { if (this.sdkClient.is_paired()) { throw new Error('Device already paired'); } const myAddress: string = this.sdkClient.get_address(); pairWith.push(myAddress); const privateData = { description: 'pairing', counter: 0, }; const publicData = { memberPublicName: userName, pairedAddresses: pairWith, }; const validation_fields: string[] = [ ...Object.keys(privateData), ...Object.keys(publicData), 'roles', ]; const roles: Record = { pairing: { members: [], validation_rules: [ { quorum: 1.0, fields: validation_fields, min_sig_member: 1.0, }, ], storages: [STORAGEURL], }, }; try { return this.createProcess(privateData, publicData, roles); } catch (e) { throw new Error(`Creating process failed:, ${e}`); } } private isFileBlob(value: any): value is { type: string; data: Uint8Array } { return ( typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array ); } private splitData(obj: Record) { const jsonCompatibleData: Record = {}; const binaryData: Record = {}; for (const [key, value] of Object.entries(obj)) { if (this.isFileBlob(value)) { binaryData[key] = value; } else { jsonCompatibleData[key] = value; } } return { jsonCompatibleData, binaryData }; } public async createProcess( privateData: Record, publicData: Record, roles: Record ): Promise { // Vérifier que les clés sont disponibles avant toute opération await this.ensureWalletKeysAvailable(); // Attendre que le relai soit prêt avec son spAddress secureLogger.info('⏳ Waiting for relays to be ready...', { component: 'Service' }); // Update UI status const { updateCreatorStatus } = await import('../utils/sp-address.utils'); updateCreatorStatus('⏳ Waiting for relays to be ready...'); await this.getRelayReadyPromise(); // Vérifier que nous avons maintenant un spAddress const relays = this.getAllRelays(); const relayAddress = relays.find(relay => relay.spAddress && relay.spAddress.trim() !== '')?.spAddress; if (!relayAddress) { secureLogger.error('Available relays:', relays, { component: 'Service' }); throw new Error('❌ No relay address available after waiting'); } secureLogger.info('✅ Relay address found:', { component: 'Service', data: relayAddress }); const feeRate = 1; // We can't encode files as the rest because Uint8Array is not valid json // So we first take them apart and we will encode them separately and put them back in the right object // TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking const privateSplitData = this.splitData(privateData); const publicSplitData = this.splitData(publicData); const encodedPrivateData = { ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), ...this.sdkClient.encode_binary(privateSplitData.binaryData), }; const encodedPublicData = { ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), ...this.sdkClient.encode_binary(publicSplitData.binaryData), }; // secureLogger.info('encodedPrivateData:', { component: 'Service', data: encodedPrivateData }); // secureLogger.info('encodedPublicData:', { component: 'Service', data: encodedPublicData }); // secureLogger.info('roles:', { component: 'Service', data: roles }); // secureLogger.info('members:', { component: 'Service', data: this.getAllMembers( })); // secureLogger.info('relayAddress:', { component: 'Service', data: relayAddress, 'feeRate:', feeRate }); // First, ensure we have a complete initial scan before requesting faucet tokens secureLogger.info('🔄 Ensuring complete initial scan before faucet request...', { component: 'Service' }); await this.ensureCompleteInitialScan(); // Now request tokens from faucet await this.getTokensFromFaucet(); const membersObj = this.getAllMembers(); secureLogger.debug('🔍 DEBUG: Members for create_new_process:', { component: 'Service', data: membersObj }); secureLogger.debug('🔍 DEBUG: Members type:', { component: 'Service', data: typeof membersObj }); secureLogger.debug('🔍 DEBUG: Members keys:', { component: 'Service', data: Object.keys(membersObj })); // Check if membersList is empty if (!membersObj || Object.keys(membersObj).length === 0) { secureLogger.warn('⚠️ No members available for create_new_process, waiting for handshake...', { component: 'Service' }); throw new Error('No members available - handshake not completed yet'); } // Convert membersObj to array format for WebAssembly (it expects a sequence, not a map) const members = Object.values(membersObj).map(member => ({ sp_addresses: member.sp_addresses })); secureLogger.debug('🔍 DEBUG: Members array length:', { component: 'Service', data: members.length }); secureLogger.debug('🔍 DEBUG: Members array sample:', { component: 'Service', data: members.slice(0, 3 })); const result = this.sdkClient.create_new_process( encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members ); if (result.updated_process) { secureLogger.info('created process:', { component: 'Service', data: result.updated_process }); await this.checkConnections(result.updated_process.current_process); return result; } else { throw new Error('Empty updated_process in createProcessReturn'); } } public async updateProcess( process: Process, privateData: Record, publicData: Record, roles: Record | null ): Promise { // If roles is null, we just take the last commited state roles if (!roles) { roles = this.getRoles(process); } else { // We should check that we have the right to change the roles here, or maybe it's better leave it to the wasm secureLogger.info('Provided new roles:', { component: 'Service', data: JSON.stringify(roles })); } const privateSplitData = this.splitData(privateData); const publicSplitData = this.splitData(publicData); const encodedPrivateData = { ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), ...this.sdkClient.encode_binary(privateSplitData.binaryData), }; const encodedPublicData = { ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), ...this.sdkClient.encode_binary(publicSplitData.binaryData), }; try { const members = Object.values(this.getAllMembers()).map(member => ({ sp_addresses: member.sp_addresses })); const result = this.sdkClient.update_process( process, encodedPrivateData, roles, encodedPublicData, members ); if (result.updated_process) { await this.checkConnections(result.updated_process.current_process); return result; } else { throw new Error('Empty updated_process in updateProcessReturn'); } } catch (e) { throw new Error(`Failed to update process: ${e}`); } } public async createPrdUpdate(processId: string, stateId: string): Promise { const process = await this.getProcess(processId); if (!process) { throw new Error('Unknown process'); } else { await this.checkConnections(process); } try { const members = Object.values(this.getAllMembers()).map(member => ({ sp_addresses: member.sp_addresses })); return this.sdkClient.create_update_message(process, stateId, members); } catch (e) { throw new Error(`Failed to create prd update: ${e}`); } } public async createPrdResponse(processId: string, stateId: string): Promise { const process = await this.getProcess(processId); if (!process) { throw new Error('Unknown process'); } try { const members = Object.values(this.getAllMembers()).map(member => ({ sp_addresses: member.sp_addresses })); return this.sdkClient.create_response_prd(process, stateId, members); } catch (e) { throw new Error(`Failed to create response prd: ${e}`); } } public async approveChange(processId: string, stateId: string): Promise { const process = await this.getProcess(processId); if (!process) { throw new Error('Failed to get process from db'); } try { const members = Object.values(this.getAllMembers()).map(member => ({ sp_addresses: member.sp_addresses })); const result = this.sdkClient.validate_state(process, stateId, members); if (result.updated_process) { await this.checkConnections(result.updated_process.current_process); return result; } else { throw new Error('Empty updated_process in approveChangeReturn'); } } catch (e) { throw new Error(`Failed to create prd response: ${e}`); } } public async rejectChange(processId: string, stateId: string): Promise { const process = await this.getProcess(processId); if (!process) { throw new Error('Failed to get process from db'); } try { return this.sdkClient.refuse_state(process, stateId); } catch (e) { throw new Error(`Failed to create prd response: ${e}`); } } async resetDevice() { this.sdkClient.reset_device(); // Clear all stores const db = await Database.getInstance(); await db.clearStore(DATABASE_CONFIG.stores.wallet.name); await db.clearStore(DATABASE_CONFIG.stores.shared_secrets.name); await db.clearStore(DATABASE_CONFIG.stores.unconfirmed_secrets.name); await db.clearStore(DATABASE_CONFIG.stores.processes.name); await db.clearStore(DATABASE_CONFIG.stores.diffs.name); } sendNewTxMessage(message: string) { sendMessage('NewTx', message); } sendCommitMessage(message: string) { sendMessage('Commit', message); } sendCipherMessages(ciphers: string[]) { for (let i = 0; i < ciphers.length; i++) { const cipher = ciphers[i]; sendMessage('Cipher', cipher); } } sendFaucetMessage(message: string): void { sendMessage('Faucet', message); } async parseCipher(message: string) { const membersList = Object.values(this.getAllMembers()).map(member => ({ sp_addresses: member.sp_addresses })); const processes = await this.getProcesses(); try { // secureLogger.info('parsing new cipher', { component: 'Service' }); const apiReturn = this.sdkClient.parse_cipher(message, membersList, processes); await this.handleApiReturn(apiReturn); // Device 1 wait Device 2 const waitingModal = document.getElementById('waiting-modal'); if (waitingModal) { this.device2Ready = true; } } catch (e) { // Log the error but don't treat it as critical during pairing process secureLogger.warn('Cipher parsing failed (this may be normal during pairing): ${e}', { component: 'Service' }); // Only log as error if it's not a pairing-related issue if (!(e as Error).message?.includes('Failed to handle decrypted message')) { secureLogger.error('Parsed cipher with error: ${e}', { component: 'Service' }); } } // await this.saveCipherTxToDb(parsedTx) } async parseNewTx(newTxMsg: string | NewTxMessage) { const parsedMsg: NewTxMessage = typeof newTxMsg === 'string' ? JSON.parse(newTxMsg) : newTxMsg; if (parsedMsg.error !== null) { secureLogger.error('Received error in new tx message:', parsedMsg.error, { component: 'Service' }); this.updateUserStatus('❌ Transaction error received'); return; } // Notify user that a transaction was received this.updateUserStatus('📨 New transaction received from blockchain...'); // Mark that we've received a transaction for waitForAmount this.hasReceivedTransaction = true; const membersList = Object.values(this.getAllMembers()).map(member => ({ sp_addresses: member.sp_addresses })); try { // Does the transaction spend the tip of a process? const prevouts = this.sdkClient.get_prevouts(parsedMsg.transaction); secureLogger.info('prevouts:', { component: 'Service', data: prevouts }); for (const process of Object.values(this.processesCache)) { const tip = process.states[process.states.length - 1].commited_in; if (prevouts.includes(tip)) { const processId = process.states[0].commited_in; const newTip = this.sdkClient.get_txid(parsedMsg.transaction); secureLogger.info('Transaction', { component: 'Service', data: newTip, 'spends the tip of process', processId }); // We take the data out of the output const newStateId = this.sdkClient.get_opreturn(parsedMsg.transaction); secureLogger.info('newStateId:', { component: 'Service', data: newStateId }); // We update the relevant process const updatedProcess = this.sdkClient.process_commit_new_state( process, newStateId, newTip ); this.processesCache[processId] = updatedProcess; secureLogger.info('updatedProcess:', { component: 'Service', data: updatedProcess }); break; } } } catch (e) { secureLogger.error('Failed to parse new tx for commitments:', e, { component: 'Service' }); } try { const parsedTx = this.sdkClient.parse_new_tx(newTxMsg, 0, membersList); if (parsedTx) { try { await this.handleApiReturn(parsedTx); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); // Force SDK to scan blocks to update wallet state after receiving tokens secureLogger.info('🔄 Forcing SDK to scan blocks to update wallet state...', { component: 'Service' }); try { // Get current device to check birthday const device = await this.getDeviceFromDatabase(); if (device?.sp_wallet) { secureLogger.debug('🔍 Device wallet state:', { component: 'Service', data: { birthday: device.sp_wallet.birthday, last_scan: device.sp_wallet.last_scan, current_block: this.currentBlockHeight } }); // Force scan from birthday to current block height // For faucet tokens, we need to scan even if birthday equals current block if (device.sp_wallet.birthday <= this.currentBlockHeight) { // For new wallets, scan from much earlier to catch faucet transactions const scanFromHeight = device.sp_wallet.birthday === this.currentBlockHeight ? Math.max(0, this.currentBlockHeight - 100) // Scan from 100 blocks earlier for new wallets : device.sp_wallet.birthday; secureLogger.info('🔄 Forcing complete scan from block ${scanFromHeight} to current block ${this.currentBlockHeight}...', { component: 'Service' }); await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL); secureLogger.info('✅ Complete block scan completed', { component: 'Service' }); // Update last_scan to current block height device.sp_wallet.last_scan = this.currentBlockHeight; await this.saveDeviceInDatabase(device); secureLogger.info('✅ Wallet last_scan updated to current block height', { component: 'Service' }); } else { secureLogger.info('🔄 Using safe scan blocks...', { component: 'Service' }); await this.safeScanBlocks(); } } else { secureLogger.info('🔄 Using safe scan blocks (no device found)...', { component: 'Service' }); await this.safeScanBlocks(); } } catch (scanError) { secureLogger.error('❌ Error during forced block scan:', scanError, { component: 'Service' }); // Fallback to safe scan try { await this.safeScanBlocks(); } catch (fallbackError) { secureLogger.error('❌ Fallback scan also failed:', fallbackError, { component: 'Service' }); } } // Check amount after scanning const updatedAmount = await this.getAmount(); secureLogger.info('💰 Amount after block scan: ${updatedAmount}', { component: 'Service' }); // Update user with scan results if (updatedAmount > 0n) { this.updateUserStatus(`💰 Wallet updated! Found ${updatedAmount} tokens`); } else { this.updateUserStatus('⏳ Transaction processed, waiting for confirmation...'); } // Additional debugging: check if SDK is properly initialized secureLogger.debug('🔍 SDK debugging info:', { component: 'Service' }); secureLogger.info('- Current block height:', { component: 'Service', data: this.currentBlockHeight }); secureLogger.info('- Blindbit URL:', { component: 'Service', data: BLINDBITURL }); secureLogger.info('- SDK client initialized:', { component: 'Service', data: !!this.sdkClient }); // Check wallet state in SDK try { const device = await this.getDeviceFromDatabase(); if (device?.sp_wallet) { secureLogger.debug('🔍 Wallet state:', { component: 'Service' }); secureLogger.info('- Last scan:', { component: 'Service', data: device.sp_wallet.last_scan }); secureLogger.info('- Current block:', { component: 'Service', data: this.currentBlockHeight }); secureLogger.info('- Scan needed:', { component: 'Service', data: device.sp_wallet.last_scan < this.currentBlockHeight }); } } catch (error) { secureLogger.error('❌ Error checking wallet state:', error, { component: 'Service' }); } } catch (scanError) { secureLogger.error('❌ Failed to scan blocks:', scanError, { component: 'Service' }); } } } catch (e) { secureLogger.debug('Error in operation', e as Error, { component: 'Service' }); } } public async handleApiReturn(apiReturn: ApiReturn) { secureLogger.debug('API return received', { component: 'Service', data: apiReturn }); if (apiReturn.partial_tx) { try { const res = this.sdkClient.sign_transaction(apiReturn.partial_tx); apiReturn.new_tx_to_send = res.new_tx_to_send; } catch (e) { secureLogger.error('Failed to sign transaction:', e, { component: 'Service' }); } } if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); await new Promise(r => setTimeout(r, 500)); } if (apiReturn.secrets) { const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets; const confirmedSecrets = apiReturn.secrets.shared_secrets; const db = await Database.getInstance(); for (const secret of unconfirmedSecrets) { await db.addObject({ storeName: 'unconfirmed_secrets', object: secret, key: null, }); } const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value })); for (const entry of entries) { try { await db.addObject({ storeName: 'shared_secrets', object: entry.value, key: entry.key, }); } catch (e) { throw e; } // We don't want to throw an error, it could simply be that we registered directly the shared secret // this.removeUnconfirmedSecret(entry.value); } } if (apiReturn.updated_process) { const updatedProcess = apiReturn.updated_process; const processId: string = updatedProcess.process_id; if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) { for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) { const blob = this.hexToBlob(cipher); try { await this.saveBlobToDb(hash, blob); } catch (e) { secureLogger.error('Failed to save blob to database', e as Error, { component: 'Service' }); } } } // Save process to db await this.saveProcessToDb(processId, updatedProcess.current_process); if (updatedProcess.diffs && updatedProcess.diffs.length != 0) { try { await this.saveDiffsToDb(updatedProcess.diffs); } catch (e) { secureLogger.error('Failed to save diffs to db:', e, { component: 'Service' }); } } } if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) { for (const hash of apiReturn.push_to_storage) { const blob = await this.getBlobFromDb(hash); if (blob) { // Get the storages from the diff data const diff = await this.getDiffByValueFromDb(hash); if (diff) { const storages = diff.storages; await this.saveDataToStorage(hash, storages, blob, null); } else { secureLogger.error('Failed to get diff from db for hash', hash, { component: 'Service' }); } } else { secureLogger.error('Failed to get data from db for hash', hash, { component: 'Service' }); } } } if (apiReturn.commit_to_send) { const commit = apiReturn.commit_to_send; this.sendCommitMessage(JSON.stringify(commit)); } if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) { this.sendCipherMessages(apiReturn.ciphers_to_send); } } public async openPairingConfirmationModal(processId: string) { const process = await this.getProcess(processId); if (!process) { secureLogger.error('Failed to find pairing process', { component: 'Service' }); return; } const firstState = process.states[0]; const roles = firstState.roles; const stateId = firstState.state_id; try { await this.routingInstance.openPairingConfirmationModal(roles, processId, stateId); } catch (e) { secureLogger.error('Error in operation', e as Error, { component: 'Service' }); } } public async waitForPairingCommitment( processId: string, maxRetries: number = 30, retryDelay: number = 2000 ): Promise { secureLogger.debug('🔍 DEBUG: waitForPairingCommitment called with processId: ${processId}', { component: 'Service' }); secureLogger.info('⏳ Waiting for pairing process ${processId} to be committed and synchronized...', { component: 'Service' }); secureLogger.info('🔄 This may take some time as we wait for SDK synchronization...', { component: 'Service' }); for (let i = 0; i < maxRetries; i++) { try { // Check device state directly without forcing updateDevice const device = this.dumpDeviceFromMemory(); secureLogger.debug('🔍 Attempt ${i + 1}/${maxRetries}: pairing_process_commitment =', { component: 'Service', data: device.pairing_process_commitment }); // Additional debugging: Check if we can get the pairing process ID let currentPairingId: string | null = null; try { currentPairingId = this.sdkClient.get_pairing_process_id(); secureLogger.debug('🔍 Current pairing process ID from SDK: ${currentPairingId}', { component: 'Service' }); } catch (e) { secureLogger.error('⚠️ SDK pairing process ID not available yet: ${(e as Error).message}', { component: 'Service' }); } // Try to force synchronization by requesting the process from peers if (i % 3 === 0 && i > 0) { try { secureLogger.info('🔄 Attempting to request process from peers...', { component: 'Service' }); await this.requestDataFromPeers(processId, [], []); secureLogger.info('✅ Process request sent to peers', { component: 'Service' }); } catch (e) { secureLogger.error('⚠️ Failed to request process from peers: ${(e as Error).message}', { component: 'Service' }); } } // For quorum=1, try to force process synchronization if (i === 2) { try { secureLogger.info('🔄 Forcing process synchronization for quorum=1 test...', { component: 'Service' }); // Force the SDK to recognize this as a pairing process const process = await this.getProcess(processId); if (process) { secureLogger.info('🔄 Process found, attempting to sync with SDK...', { component: 'Service' }); // Try to trigger SDK synchronization await this.sdkClient.get_pairing_process_id(); } } catch (e) { secureLogger.error('⚠️ Process synchronization attempt failed: ${(e as Error).message}', { component: 'Service' }); } } // Check if the process exists in our processes list try { const process = await this.getProcess(processId); if (process) { secureLogger.debug('🔍 Process exists: ${processId}, states: ${process.states?.length || 0}', { component: 'Service' }); const lastState = process.states?.[process.states.length - 1]; if (lastState) { secureLogger.debug('🔍 Last state ID: ${lastState.state_id}', { component: 'Service' }); } } else { secureLogger.warn('⚠️ Process not found in local processes: ${processId}', { component: 'Service' }); } } catch (e) { secureLogger.error('⚠️ Error checking process: ${(e as Error).message}', { component: 'Service' }); } // Check WebSocket connection and handshake data try { secureLogger.debug('🔍 WebSocket connections: ${Object.keys(this.relayAddresses).length} relays', { component: 'Service' }); secureLogger.debug('🔍 Current block height: ${this.currentBlockHeight}', { component: 'Service' }); secureLogger.debug('🔍 Members list size: ${Object.keys(this.membersList).length}', { component: 'Service' }); } catch (e) { secureLogger.error('⚠️ Error checking WebSocket state: ${(e as Error).message}', { component: 'Service' }); } // Check if the commitment is set and not null/empty if ( device.pairing_process_commitment && device.pairing_process_commitment !== null && device.pairing_process_commitment !== '' ) { secureLogger.info('✅ Pairing process commitment found:', { component: 'Service', data: device.pairing_process_commitment }); return; } // For quorum=1.0 processes, the creator must commit themselves // Check if the process is ready for the creator to commit if (currentPairingId && currentPairingId === processId) { secureLogger.info('✅ Creator process is synchronized and ready for self-commitment (quorum=1.0)', { component: 'Service' }); return; } // For quorum=1 test, if we have a process but no commitment yet, // try to force synchronization by calling updateDevice more frequently if (i < 5) { try { await this.updateDevice(); secureLogger.info('🔄 Forced device update on attempt ${i + 1}', { component: 'Service' }); } catch (e) { secureLogger.error('⚠️ Forced device update failed: ${(e as Error).message}', { component: 'Service' }); } } // If we have the process but SDK doesn't know about it yet, try to force SDK sync if (currentPairingId === null && i > 2) { try { secureLogger.info('🔄 Attempting to force SDK synchronization for process ${processId}...', { component: 'Service' }); // Try to manually pair the device with the process const process = await this.getProcess(processId); if (process && process.states && process.states.length > 0) { const lastState = process.states[process.states.length - 1]; if (lastState.public_data && lastState.public_data['pairedAddresses']) { const pairedAddresses = this.decodeValue(lastState.public_data['pairedAddresses']); secureLogger.info('🔄 Manually pairing device with addresses: ${JSON.stringify(pairedAddresses)}', { component: 'Service' }); this.sdkClient.pair_device(processId, pairedAddresses); secureLogger.info('✅ Manual pairing completed', { component: 'Service' }); } } } catch (e) { secureLogger.error('⚠️ Manual pairing failed: ${(e as Error).message}', { component: 'Service' }); } } secureLogger.info('⏳ Still waiting for SDK synchronization... (${i + 1}/${maxRetries})', { component: 'Service' }); // Only try updateDevice every 5 attempts to avoid spam if (i % 5 === 0 && i > 0) { try { await this.updateDevice(); secureLogger.info('✅ Device update successful on attempt ${i + 1}', { component: 'Service' }); } catch (e) { secureLogger.error('⚠️ Device update failed on attempt ${i + 1} (process may not be committed yet): ${(e as Error).message}', { component: 'Service' }); } } } catch (e) { secureLogger.error('❌ Attempt ${i + 1}/${maxRetries}: Error during synchronization - ${(e as Error).message}', { component: 'Service' }); } if (i < maxRetries - 1) { secureLogger.info('⏳ Waiting ${retryDelay}ms before next attempt...', { component: 'Service' }); await new Promise(resolve => setTimeout(resolve, retryDelay)); } } throw new Error( `❌ Pairing process ${processId} was not synchronized after ${maxRetries} attempts (${(maxRetries * retryDelay) / 1000}s)` ); } public async confirmPairing(pairingId?: string) { try { // Is the wasm paired? secureLogger.info('confirmPairing', { component: 'Service' }); let processId: string; if (pairingId) { processId = pairingId; secureLogger.info('pairingId (provided):', { component: 'Service', data: processId }); } else if (this.processId) { processId = this.processId; secureLogger.info('pairingId (from stored processId):', { component: 'Service', data: processId }); } else { // Try to get pairing process ID, with retry if it fails let retries = 3; while (retries > 0) { try { processId = this.getPairingProcessId(); secureLogger.info('pairingId (from SDK):', { component: 'Service', data: processId }); break; } catch (e) { retries--; if (retries === 0) { throw e; } secureLogger.error('Failed to get pairing process ID, retrying... (${retries} attempts left)', { component: 'Service' }); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second before retry } } } // TODO confirm that the pairing process id is known, commited const newDevice = this.dumpDeviceFromMemory(); secureLogger.info('newDevice:', { component: 'Service', data: newDevice }); await this.saveDeviceInDatabase(newDevice); secureLogger.info('Device saved in database', { component: 'Service' }); } catch (e) { secureLogger.error('Failed to confirm pairing', { component: 'Service' }); return; } } public async updateDevice(): Promise { let myPairingProcessId: string; try { myPairingProcessId = this.getPairingProcessId(); } catch (e) { secureLogger.error('Failed to get pairing process id', { component: 'Service' }); return; } const myPairingProcess = await this.getProcess(myPairingProcessId); if (!myPairingProcess) { secureLogger.error('Unknown pairing process', { component: 'Service' }); return; } const myPairingState = this.getLastCommitedState(myPairingProcess); if (myPairingState) { const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; const spAddressList = this.decodeValue(encodedSpAddressList); if (spAddressList.length === 0) { secureLogger.error('Empty pairedAddresses', { component: 'Service' }); return; } // We can check if our address is included and simply unpair if it's not if (!spAddressList.includes(await this.getDeviceAddress())) { await this.unpairDevice(); return; } // We can update the device with the new addresses this.sdkClient.unpair_device(); this.sdkClient.pair_device(myPairingProcessId, spAddressList); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); } } public pairDevice(processId: string, spAddressList: string[]): void { try { this.sdkClient.pair_device(processId, spAddressList); } catch (e) { throw new Error(`Failed to pair device: ${e}`); } } /** * Vérifie que les clés du wallet sont disponibles avant toute opération */ private async ensureWalletKeysAvailable(): Promise { try { const device = this.dumpDeviceFromMemory(); if (!device?.sp_wallet) { throw new Error('❌ Wallet not initialized - WebAuthn decryption required'); } // Vérifier si les clés sont déjà disponibles dans le device if (device.sp_wallet.spend_key && device.sp_wallet.scan_sk) { secureLogger.info('✅ Wallet keys available for operation', { component: 'Service' }); return; } // Si les clés ne sont pas disponibles, restaurer le device complet depuis la base de données secureLogger.info('🔐 Wallet keys not in memory, restoring full device from database...', { component: 'Service' }); try { // Récupérer le device complet depuis la base de données (contient birthday, last_scan, etc.) let deviceFromDb; try { deviceFromDb = await this.getDeviceFromDatabase(); } catch (dbError) { secureLogger.error('❌ Failed to get device from database:', dbError, { component: 'Service' }); throw new Error(`❌ Database error: ${dbError}`); } if (!deviceFromDb?.sp_wallet) { throw new Error('Device not found in database'); } secureLogger.debug('🔍 Device from database - birthday: ${deviceFromDb.sp_wallet.birthday}, last_scan: ${deviceFromDb.sp_wallet.last_scan}', { component: 'Service' }); // Récupérer les credentials pour injecter les clés let credentials; try { const { SecureCredentialsService } = await import('./secure-credentials.service'); const secureCredentialsService = SecureCredentialsService.getInstance(); credentials = await secureCredentialsService.retrieveCredentials(''); } catch (credentialError) { secureLogger.error('❌ Failed to retrieve credentials:', credentialError, { component: 'Service' }); throw new Error(`❌ Credential retrieval failed: ${credentialError}`); } secureLogger.debug('🔍 Credentials to inject:', { component: 'Service', data: { spendKey_length: credentials.spendKey?.length || 0, scanKey_length: credentials.scanKey?.length || 0, spendKey_type: typeof credentials.spendKey, scanKey_type: typeof credentials.scanKey } }); if (!credentials) { throw new Error('No credentials found'); } // Injecter les clés dans le device de la base de données avant restauration secureLogger.info('🔧 Injecting keys into device from database...', { component: 'Service' }); try { if (deviceFromDb?.sp_wallet) { secureLogger.debug('🔍 Device from database before injection:', { component: 'Service', data: { has_spend_key: !!deviceFromDb.sp_wallet.spend_key, has_scan_sk: !!deviceFromDb.sp_wallet.scan_sk, spend_key_type: typeof deviceFromDb.sp_wallet.spend_key, scan_sk_type: typeof deviceFromDb.sp_wallet.scan_sk } }); // Injecter les clés dans le device de la base de données deviceFromDb.sp_wallet.spend_key = {Secret: credentials.spendKey}; deviceFromDb.sp_wallet.scan_sk = credentials.scanKey; secureLogger.debug('🔍 Device from database after injection:', { component: 'Service', data: { has_spend_key: !!deviceFromDb.sp_wallet.spend_key, has_scan_sk: !!deviceFromDb.sp_wallet.scan_sk, spend_key_type: typeof deviceFromDb.sp_wallet.spend_key, scan_sk_type: typeof deviceFromDb.sp_wallet.scan_sk, spend_key_secret: deviceFromDb.sp_wallet.spend_key?.Secret ? 'present' : 'missing', scan_sk_length: deviceFromDb.sp_wallet.scan_sk?.length || 0 } }); } else { throw new Error('No sp_wallet found in device from database'); } } catch (injectError) { secureLogger.error('❌ Failed to inject keys into device from database:', injectError, { component: 'Service' }); throw new Error(`❌ Key injection failed: ${injectError}`); } // Restaurer le device complet avec les clés injectées secureLogger.info('🔧 Restoring complete device with injected keys...', { component: 'Service' }); try { this.restoreDevice(deviceFromDb); secureLogger.info('✅ Device restored successfully with keys', { component: 'Service' }); } catch (restoreError) { secureLogger.error('❌ Failed to restore device:', restoreError, { component: 'Service' }); throw new Error(`❌ Device restore failed: ${restoreError}`); } // Le SDK a déjà les bonnes clés en mémoire après restoreDevice(deviceFromDb) // Vérifier que les clés sont présentes dans le SDK secureLogger.info('🔧 Verifying keys in SDK after device restoration...', { component: 'Service' }); try { const currentDevice = this.dumpDeviceFromMemory(); secureLogger.debug('🔍 Device state after restoration:', { component: 'Service', data: { has_spend_key: !!currentDevice.sp_wallet?.spend_key, has_scan_sk: !!currentDevice.sp_wallet?.scan_sk, spend_key_type: typeof currentDevice.sp_wallet?.spend_key, scan_sk_type: typeof currentDevice.sp_wallet?.scan_sk } }); // Si les clés ne sont pas présentes, c'est normal car le SDK les gère en interne // Le SDK utilise ses propres clés pour les opérations, pas celles du device JavaScript secureLogger.info('✅ Device restored with SDK-managed keys', { component: 'Service' }); } catch (verifyError) { secureLogger.error('❌ Failed to verify device state:', verifyError, { component: 'Service' }); throw new Error(`❌ Device verification failed: ${verifyError}`); } secureLogger.info('✅ Device restored with updated keys from credentials', { component: 'Service' }); } catch (credentialError) { secureLogger.error('❌ Failed to restore wallet keys from credentials:', credentialError, { component: 'Service' }); throw new Error(`❌ Wallet keys not available: ${credentialError}`); } } catch (error) { secureLogger.error('❌ Wallet keys not available:', error, { component: 'Service' }); throw new Error('❌ Wallet keys not available - WebAuthn decryption required'); } } public async getAmount(): Promise { if (!this.sdkClient) { throw new Error('SDK not initialized - cannot get amount'); } // Vérifier que les clés sont disponibles avant toute opération await this.ensureWalletKeysAvailable(); try { const amount = this.sdkClient.get_available_amount(); secureLogger.info('💰 SDK get_available_amount() returned: ${amount}', { component: 'Service' }); // Additional debugging: check wallet state try { const device = this.dumpDeviceFromMemory(); if (device?.sp_wallet) { secureLogger.debug('🔍 Wallet debugging info:', { component: 'Service', data: { birthday: device.sp_wallet.birthday, last_scan: device.sp_wallet.last_scan, current_block: this.currentBlockHeight, has_spend_key: !!device.sp_wallet.spend_key, has_scan_key: !!device.sp_wallet.scan_sk } }); } } catch (error) { secureLogger.warn('⚠️ Error getting wallet debugging info:', { component: 'Service', data: error }); } return amount; } catch (error) { secureLogger.error('❌ Error calling get_available_amount():', error, { component: 'Service' }); throw error; } } async getDeviceAddress(): Promise { try { if (!this.sdkClient) { throw new Error('WebAssembly SDK not initialized - memory too high'); } // Vérifier que les clés sont disponibles avant toute opération await this.ensureWalletKeysAvailable(); return this.sdkClient.get_address(); } catch (e) { throw new Error(`Failed to get device address: ${e}`); } } getCurrentBlockHeight(): number { return this.currentBlockHeight; } public dumpDeviceFromMemory(): Device { try { return this.sdkClient.dump_device(); } catch (e) { throw new Error(`Failed to dump device: ${e}`); } } public dumpNeuteredDevice(): Device | null { try { return this.sdkClient.dump_neutered_device(); } catch (e) { secureLogger.error('Failed to dump device: ${e}', { component: 'Service' }); return null; } } public getPairingProcessId(): string { try { return this.sdkClient.get_pairing_process_id(); } catch (e) { throw new Error(`Failed to get pairing process: ${e}`); } } async saveDeviceInDatabase(device: Device): Promise { secureLogger.info('🔐 saveDeviceInDatabase called - starting encryption process...', { component: 'Service' }); try { // Récupérer la clé PBKDF2 pour chiffrer le device secureLogger.info('🔐 Retrieving PBKDF2 key for device encryption...', { component: 'Service' }); const { SecureCredentialsService } = await import('./secure-credentials.service'); const secureCredentialsService = SecureCredentialsService.getInstance(); // Trouver le mode de sécurité qui fonctionne const allSecurityModes = ['none', 'otp', 'password', 'os', 'proton-pass']; let pbkdf2Key: string | null = null; let workingMode: string | null = null; for (const mode of allSecurityModes) { try { const hasKey = await secureCredentialsService.hasPBKDF2Key(mode as any); if (hasKey) { const key = await secureCredentialsService.retrievePBKDF2Key(mode as any); if (key) { pbkdf2Key = key; workingMode = mode; secureLogger.info('✅ PBKDF2 key found for mode: ${mode}', { component: 'Service' }); break; } } } catch (e) { // Continue to next mode secureLogger.warn('⚠️ No PBKDF2 key for mode ${mode}', { component: 'Service' }); } } if (!pbkdf2Key) { throw new Error('Failed to retrieve PBKDF2 key - cannot encrypt device'); } secureLogger.info('🔐 Encrypting device with PBKDF2 key...', { component: 'Service' }); // Chiffrer le device const { EncryptionService } = await import('./encryption.service'); const encryptionService = EncryptionService.getInstance(); const deviceString = JSON.stringify(device); const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key); secureLogger.info('✅ Device encrypted successfully', { component: 'Service' }); // Récupérer le wallet existant pour préserver encrypted_wallet secureLogger.debug('🔍 Retrieving existing wallet to preserve encrypted_wallet...', { component: 'Service' }); let encryptedWallet: string | undefined = undefined; // Récupérer le wallet chiffré depuis la base de données directement const dbTemp = await new Promise((resolve, reject) => { const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const walletStoreName = DATABASE_CONFIG.stores.wallet.name; const walletData = await new Promise((resolve, reject) => { const tx = dbTemp.transaction(walletStoreName, 'readonly'); const store = tx.objectStore(walletStoreName); const getRequest = store.get('1'); getRequest.onsuccess = () => resolve(getRequest.result); getRequest.onerror = () => reject(getRequest.error); }); if (walletData?.encrypted_wallet) { encryptedWallet = walletData.encrypted_wallet; secureLogger.info('✅ encrypted_wallet preserved', { component: 'Service' }); } else { secureLogger.warn('⚠️ No existing encrypted_wallet to preserve', { component: 'Service' }); } // Sauvegarder avec le format chiffré secureLogger.info('💾 Saving encrypted device to database...', { component: 'Service' }); // Utiliser directement IndexedDB au lieu du service Database pour éviter les problèmes de service worker const db = await new Promise((resolve, reject) => { const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const walletStore = DATABASE_CONFIG.stores.wallet.name; // Sauvegarder le wallet chiffré directement avec IndexedDB await new Promise((resolve, reject) => { const transaction = db.transaction([walletStore], 'readwrite'); const store = transaction.objectStore(walletStore); // Sauvegarder le nouveau wallet chiffré (put écrase automatiquement si existe) const walletObject: any = { pre_id: '1', encrypted_device: encryptedDevice }; // Préserver encrypted_wallet s'il existe if (encryptedWallet) { walletObject.encrypted_wallet = encryptedWallet; } // Définir les handlers de transaction avant le put transaction.oncomplete = async () => { secureLogger.info('✅ Transaction completed for wallet save', { component: 'Service' }); // Vérifier que le wallet a bien été sauvegardé en le récupérant depuis la base try { const verificationDb = await new Promise((resolveDb, rejectDb) => { const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version); request.onsuccess = () => resolveDb(request.result); request.onerror = () => rejectDb(request.error); }); const verificationTx = verificationDb.transaction([walletStore], 'readonly'); const verificationStore = verificationTx.objectStore(walletStore); const verifyRequest = verificationStore.get('1'); await new Promise((resolveVerify, rejectVerify) => { verifyRequest.onsuccess = () => { const savedData = verifyRequest.result; if (savedData?.encrypted_device === encryptedDevice) { secureLogger.info('✅ Verified: Device correctly saved in database', { component: 'Service' }); resolveVerify(); } else { secureLogger.error('❌ Verification failed: Device not found or encrypted data mismatch', { component: 'Service' }); rejectVerify(new Error('Device save verification failed')); } }; verifyRequest.onerror = () => { secureLogger.error('❌ Verification failed: Could not retrieve saved device', verifyRequest.error, { component: 'Service' }); rejectVerify(verifyRequest.error); }; }); resolve(); } catch (verifyError) { reject(verifyError); } }; transaction.onerror = () => { secureLogger.error('❌ Transaction failed:', transaction.error, { component: 'Service' }); reject(transaction.error); }; const putRequest = store.put(walletObject); putRequest.onsuccess = () => { secureLogger.info('✅ Device saved to database with encryption', { component: 'Service' }); // La vérification se fera dans transaction.oncomplete }; putRequest.onerror = () => { secureLogger.error('❌ Failed to save wallet:', putRequest.error, { component: 'Service' }); reject(putRequest.error); }; }); } catch (e) { secureLogger.error('❌ Error saving device to database:', e, { component: 'Service' }); throw e; } } async getDeviceFromDatabase(): Promise { secureLogger.debug('getDeviceFromDatabase - attempting to get wallet with key "1"', { component: 'Service' }); // Utiliser directement IndexedDB au lieu du service Database pour éviter les problèmes de service worker const db = await new Promise((resolve, reject) => { const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const walletStore = DATABASE_CONFIG.stores.wallet.name; secureLogger.debug('🔍 DEBUG: getDeviceFromDatabase - db opened directly, objectStoreNames:', { component: 'Service', data: Array.from(db.objectStoreNames })); try { const dbRes = await new Promise((resolve, reject) => { const tx = db.transaction(walletStore, 'readonly'); const store = tx.objectStore(walletStore); const getRequest = store.get('1'); getRequest.onsuccess = () => resolve(getRequest.result); getRequest.onerror = () => reject(getRequest.error); }); secureLogger.debug('🔍 DEBUG: getDeviceFromDatabase - db.getObject result:', { component: 'Service', data: dbRes }); if (!dbRes) { secureLogger.debug('getDeviceFromDatabase - no data found for key "1"', { component: 'Service' }); return null; } // Check if data is encrypted (new format) or plain (old format) if (dbRes['encrypted_device']) { // New encrypted format - need to decrypt secureLogger.info('🔐 Wallet found in encrypted format, decrypting...', { component: 'Service' }); // Get the PBKDF2 key based on security mode const { SecureCredentialsService } = await import('./secure-credentials.service'); const secureCredentialsService = SecureCredentialsService.getInstance(); // Get all security modes to find which one works // Mettre 'none' en premier pour éviter d'ouvrir la fenêtre du navigateur const allSecurityModes = ['none', 'otp', 'password', 'os', 'proton-pass']; let pbkdf2Key: string | null = null; let workingMode: string | null = null; for (const mode of allSecurityModes) { try { // Vérifier d'abord silencieusement si une clé existe const hasKey = await secureCredentialsService.hasPBKDF2Key(mode as any); if (hasKey) { // Si une clé existe, essayer de la récupérer const key = await secureCredentialsService.retrievePBKDF2Key(mode as any); if (key) { pbkdf2Key = key; workingMode = mode; break; } } } catch (e) { // Continue to next mode } } if (!pbkdf2Key) { throw new Error('Failed to retrieve PBKDF2 key - cannot decrypt wallet'); } // Decrypt the device const { EncryptionService } = await import('./encryption.service'); const encryptionService = EncryptionService.getInstance(); const decryptedDeviceString = await encryptionService.decrypt( dbRes['encrypted_device'], pbkdf2Key ); const decryptedDevice = JSON.parse(decryptedDeviceString); secureLogger.info('✅ Wallet decrypted successfully', { component: 'Service' }); return decryptedDevice; } else if (dbRes['device']) { // Old plain format (backward compatibility) secureLogger.warn('⚠️ Wallet found in old format (not encrypted)', { component: 'Service' }); return dbRes['device']; } else { return null; } } catch (e) { throw new Error(`Failed to retrieve device from db: ${e}`); } } async deleteAccount(): Promise { const db = await Database.getInstance(); try { // Clear all stores await db.clearStore(DATABASE_CONFIG.stores.wallet.name); await db.clearStore(DATABASE_CONFIG.stores.processes.name); await db.clearStore(DATABASE_CONFIG.stores.shared_secrets.name); await db.clearStore(DATABASE_CONFIG.stores.unconfirmed_secrets.name); await db.clearStore(DATABASE_CONFIG.stores.diffs.name); await db.clearStore(DATABASE_CONFIG.stores.data.name); await db.clearStore(DATABASE_CONFIG.stores.labels.name); // Clear localStorage localStorage.clear(); sessionStorage.clear(); // Clear IndexedDB completely await this.clearAllIndexedDB(); secureLogger.info('✅ Account completely deleted', { component: 'Service' }); } catch (e) { secureLogger.error('❌ Error deleting account:', e, { component: 'Service' }); throw new Error(`Failed to delete account: ${e}`); } } private async clearAllIndexedDB(): Promise { return new Promise((resolve, reject) => { const deleteReq = indexedDB.deleteDatabase('4nk'); deleteReq.onsuccess = () => { secureLogger.info('✅ IndexedDB database deleted', { component: 'Service' }); resolve(); }; deleteReq.onerror = () => { secureLogger.error('❌ Error deleting IndexedDB database', { component: 'Service' }); reject(deleteReq.error); }; }); } async getMemberFromDevice(): Promise { try { const device = await this.getDeviceFromDatabase(); if (device) { const pairedMember = device['paired_member']; return pairedMember.sp_addresses; } else { return null; } } catch (e) { throw new Error(`Failed to retrieve paired_member from device: ${e}`); } } isChildRole(parent: any, child: any): boolean { try { this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child)); } catch (e) { secureLogger.error('Error in operation', e as Error, { component: 'Service' }); return false; } return true; } rolesContainsUs(roles: Record): boolean { let us; try { us = this.sdkClient.get_pairing_process_id(); } catch (e) { throw e; } return this.rolesContainsMember(roles, us); } rolesContainsMember(roles: Record, pairingProcessId: string): boolean { for (const roleDef of Object.values(roles)) { if (roleDef.members.includes(pairingProcessId)) { return true; } } return false; } async dumpWallet() { const wallet = await this.sdkClient.dump_wallet(); return wallet; } public createFaucetMessage() { const message = this.sdkClient.create_faucet_msg(); return message; } async createNewDevice() { let spAddress = ''; try { if (!this.sdkClient) { throw new Error('WebAssembly SDK not initialized - cannot create device'); } // We set birthday later when we have the chain tip from relay secureLogger.info('🔧 Creating new device with birthday 0...', { component: 'Service' }); spAddress = await this.sdkClient.create_new_device(0, 'signet'); secureLogger.info('✅ Device created with address:', { component: 'Service', data: spAddress }); // Force wallet generation to ensure keys are created secureLogger.info('🔧 Forcing wallet generation...', { component: 'Service' }); try { const wallet = await this.sdkClient.dump_wallet(); secureLogger.info('✅ Wallet generated:', { component: 'Service', data: wallet }); } catch (walletError) { secureLogger.warn('⚠️ Wallet generation failed:', { component: 'Service', data: walletError }); } const device = this.dumpDeviceFromMemory(); secureLogger.debug('🔍 Device details after creation:', { component: 'Service', data: { has_spend_key: !!device.sp_wallet?.spend_key, has_scan_key: !!device.sp_wallet?.scan_sk, birthday: device.sp_wallet?.birthday, sp_address: device.sp_wallet?.address } }); await this.saveDeviceInDatabase(device); secureLogger.info('✅ Device saved to database', { component: 'Service' }); } catch (e) { secureLogger.error('Services ~ Error:', e, { component: 'Service' }); } return spAddress; } public restoreDevice(device: Device) { try { this.sdkClient.restore_device(device); secureLogger.info('✅ Device restored successfully', { component: 'Service' }); } catch (e) { secureLogger.error('❌ Error restoring device:', e, { component: 'Service' }); } } public async waitForBlockHeight(): Promise { secureLogger.info('⏳ Waiting for block height to be set...', { component: 'Service' }); // Wait up to 10 seconds for block height to be set let attempts = 0; const maxAttempts = 20; // 10 seconds with 500ms intervals while (this.currentBlockHeight === -1 && attempts < maxAttempts) { attempts++; secureLogger.info('⏳ Waiting for block height... (attempt ${attempts}/${maxAttempts})', { component: 'Service' }); await new Promise(resolve => setTimeout(resolve, 500)); } if (this.currentBlockHeight === -1) { throw new Error('Timeout waiting for block height from relay'); } secureLogger.info('✅ Block height received: ${this.currentBlockHeight}', { component: 'Service' }); } /** * Ensures a complete initial scan is performed before requesting faucet tokens * This prevents the race condition between scan and faucet transactions * Only performs scan if wallet is not already synchronized */ public async ensureCompleteInitialScan(): Promise { secureLogger.info('🔄 Ensuring complete initial scan...', { component: 'Service' }); try { const device = await this.getDeviceFromDatabase(); if (!device?.sp_wallet) { throw new Error('Device not found or wallet not initialized'); } // Check if wallet is already synchronized const lastScan = device.sp_wallet.last_scan || 0; const isSynchronized = lastScan >= this.currentBlockHeight; if (isSynchronized) { secureLogger.info('✅ Wallet already synchronized (last_scan: ${lastScan}, current: ${this.currentBlockHeight})', { component: 'Service' }); return; } // Only scan if wallet is not synchronized secureLogger.info('🔄 Wallet needs synchronization (last_scan: ${lastScan}, current: ${this.currentBlockHeight})', { component: 'Service' }); secureLogger.info('🔄 Performing scan from block ${lastScan} to ${this.currentBlockHeight}...', { component: 'Service' }); await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL); secureLogger.info('✅ Complete initial scan completed', { component: 'Service' }); // Update last_scan to current block height device.sp_wallet.last_scan = this.currentBlockHeight; await this.saveDeviceInDatabase(device); secureLogger.info('✅ Wallet scan state updated', { component: 'Service' }); } catch (error) { secureLogger.error('❌ Error during complete initial scan:', error, { component: 'Service' }); throw error; } } public async updateDeviceBlockHeight(): Promise { if (this.currentBlockHeight === -1) { throw new Error('Current block height not set'); } // Update user status this.updateUserStatus('🔄 Synchronizing wallet with blockchain...'); let device: Device | null = null; try { device = await this.getDeviceFromDatabase(); } catch (e) { throw new Error(`Failed to get device from database: ${e}`); } if (!device) { throw new Error('Device not found'); } const birthday = device.sp_wallet.birthday; if (birthday === undefined || birthday === null) { throw new Error('Birthday not found'); } if (birthday === 0) { // This is a new device, set birthday to scan from much earlier to catch faucet transactions // Scan from 100 blocks earlier to ensure we catch all faucet transactions secureLogger.info('🔧 Updating birthday for new device:', { component: 'Service', data: { old_birthday: device.sp_wallet.birthday, new_birthday: Math.max(0, this.currentBlockHeight - 100 }), current_block: this.currentBlockHeight }); device.sp_wallet.birthday = Math.max(0, this.currentBlockHeight - 100); // We also set last_scan to the same value initially device.sp_wallet.last_scan = device.sp_wallet.birthday; try { // First set the updated device in memory this.sdkClient.restore_device(device); // Vérifier que le device a été restauré en mémoire const restoredDevice = this.dumpDeviceFromMemory(); if (restoredDevice?.sp_wallet?.birthday === device.sp_wallet.birthday) { secureLogger.info('✅ Device restored in memory with updated birthday:', { component: 'Service', data: device.sp_wallet.birthday }); } else { throw new Error(`Device restoration failed: expected birthday ${device.sp_wallet.birthday}, got ${restoredDevice?.sp_wallet?.birthday}`); } // Then save it to database await this.saveDeviceInDatabase(device); // Vérifier que le device a été sauvegardé en base de données const savedDevice = await this.getDeviceFromDatabase(); if (savedDevice?.sp_wallet?.birthday === device.sp_wallet.birthday) { secureLogger.info('✅ Device saved to database with updated birthday:', { component: 'Service', data: device.sp_wallet.birthday }); } else { throw new Error(`Device save verification failed: expected birthday ${device.sp_wallet.birthday}, got ${savedDevice?.sp_wallet?.birthday}`); } // For new wallets, perform initial scan to catch any existing transactions secureLogger.info('🔄 Performing initial scan for new wallet from block ${device.sp_wallet.birthday} to ${this.currentBlockHeight}...', { component: 'Service' }); await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL); // Vérifier que le scan est terminé en vérifiant last_scan const deviceAfterScan = this.dumpDeviceFromMemory(); if (deviceAfterScan?.sp_wallet?.last_scan === this.currentBlockHeight) { secureLogger.info('✅ Initial scan completed for new wallet', { component: 'Service' }); } else { secureLogger.warn('⚠️ Initial scan may not be complete: expected last_scan ${this.currentBlockHeight}, got ${deviceAfterScan?.sp_wallet?.last_scan}', { component: 'Service' }); } // Update last_scan to current block height device.sp_wallet.last_scan = this.currentBlockHeight; await this.saveDeviceInDatabase(device); // Vérifier que le device a été sauvegardé avec last_scan mis à jour const finalDevice = await this.getDeviceFromDatabase(); if (finalDevice?.sp_wallet?.last_scan === this.currentBlockHeight) { secureLogger.info('✅ New wallet initial scan completed and saved', { component: 'Service' }); } else { throw new Error(`Final save verification failed: expected last_scan ${this.currentBlockHeight}, got ${finalDevice?.sp_wallet?.last_scan}`); } // Note: Faucet tokens will be requested later in the pairing process // when credentials are available secureLogger.info('🪙 Faucet tokens will be requested during pairing process', { component: 'Service' }); secureLogger.info('✅ updateDeviceBlockHeight completed successfully for new device', { component: 'Service' }); return; } catch (e) { throw new Error(`Failed to save updated device: ${e}`); } } else { // This is existing device, we need to catch up if last_scan is lagging behind chain_tip if (device.sp_wallet.last_scan < this.currentBlockHeight) { // We need to catch up - this is the initial synchronization, not a duplicate scan secureLogger.info('🔄 Initial wallet synchronization with blockchain...', { component: 'Service' }); try { await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL); secureLogger.info('✅ Initial wallet synchronization completed', { component: 'Service' }); } catch (e) { secureLogger.error('Failed to scan blocks: ${e}', { component: 'Service' }); return; } // If everything went well, we can update our storage try { const device = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(device); this.updateUserStatus('✅ Wallet synchronized with blockchain'); } catch (e) { secureLogger.error('Failed to save updated device: ${e}', { component: 'Service' }); this.updateUserStatus('⚠️ Wallet synchronization completed with warnings'); } } else { // Up to date, just returns this.updateUserStatus('✅ Wallet already synchronized'); return; } } } private async removeProcess(processId: string): Promise { const db = await Database.getInstance(); const storeName = 'processes'; try { await db.deleteObject(storeName, processId); } catch (e) { secureLogger.error('Error in operation', e as Error, { component: 'Service' }); } } public async batchSaveProcessesToDb(processes: Record) { if (Object.keys(processes).length === 0) { return; } const db = await Database.getInstance(); const storeName = 'processes'; try { await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })), }); this.processesCache = { ...this.processesCache, ...processes }; } catch (e) { throw e; } } public async saveProcessToDb(processId: string, process: Process) { const db = await Database.getInstance(); const storeName = 'processes'; try { await db.addObject({ storeName, object: process, key: processId, }); // Update the process in the cache this.processesCache[processId] = process; } catch (e) { secureLogger.error('Failed to save process ${processId}: ${e}', { component: 'Service' }); } } public async saveBlobToDb(hash: string, data: Blob) { const db = await Database.getInstance(); try { await db.addObject({ storeName: 'data', object: data, key: hash, }); } catch (e) { secureLogger.error('Failed to save data to db: ${e}', { component: 'Service' }); } } public async getBlobFromDb(hash: string): Promise { const db = await Database.getInstance(); try { return await db.getObject('data', hash); } catch (e) { return null; } } public async saveDataToStorage(hash: string, storages: string[], data: Blob, ttl: number | null) { try { await storeData(storages, hash, data, ttl); } catch (e) { secureLogger.error('Failed to store data with hash ${hash}: ${e}', { component: 'Service' }); } } public async fetchValueFromStorage(hash: string): Promise { const storages = [STORAGEURL]; return await retrieveData(storages, hash); } public async getDiffByValueFromDb(hash: string): Promise { const db = await Database.getInstance(); const diff = await db.getObject('diffs', hash); return diff; } public async saveDiffsToDb(diffs: UserDiff[]) { const db = await Database.getInstance(); try { for (const diff of diffs) { await db.addObject({ storeName: 'diffs', object: diff, key: null, }); } } catch (e) { throw new Error(`Failed to save process: ${e}`); } } public async getProcess(processId: string): Promise { if (!processId) { return null; } if (this.processesCache[processId]) { return this.processesCache[processId]; } else { const db = await Database.getInstance(); const process = await db.getObject('processes', processId); return process; } } public async getProcesses(): Promise> { if (Object.keys(this.processesCache).length > 0) { return this.processesCache; } else { try { const db = await Database.getInstance(); this.processesCache = await db.dumpStore('processes'); return this.processesCache; } catch (e) { throw e; } } } public async restoreProcessesFromBackUp(processes: Record) { const db = await Database.getInstance(); const storeName = 'processes'; try { await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })), }); } catch (e) { throw e; } await this.restoreProcessesFromDB(); } // Restore processes cache from persistent storage public async restoreProcessesFromDB() { const db = await Database.getInstance(); try { const processes: Record = await db.dumpStore('processes'); if (processes && Object.keys(processes).length != 0) { secureLogger.info('Restoring ${Object.keys(processes).length} processes', { component: 'Service' }); this.processesCache = processes; } else { secureLogger.info('No processes to restore!', { component: 'Service' }); } } catch (e) { throw e; } } public async clearSecretsFromDB() { const db = await Database.getInstance(); try { await db.clearStore('shared_secrets'); await db.clearStore('unconfirmed_secrets'); } catch (e) { secureLogger.error('Error in operation', e as Error, { component: 'Service' }); } } public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { const db = await Database.getInstance(); for (const secret of secretsStore.unconfirmed_secrets) { await db.addObject({ storeName: 'unconfirmed_secrets', object: secret, key: null, }); } const entries = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value, })); for (const entry of entries) { await db.addObject({ storeName: 'shared_secrets', object: entry.value, key: entry.key, }); } // Now we can transfer them to memory await this.restoreSecretsFromDB(); } public async restoreSecretsFromDB() { const db = await Database.getInstance(); try { if (!this.sdkClient) { secureLogger.warn('WebAssembly SDK not initialized - skipping secrets restoration', { component: 'Service' }); return; } const sharedSecrets: Record = await db.dumpStore('shared_secrets'); const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); const secretsStore = { shared_secrets: sharedSecrets, unconfirmed_secrets: Object.values(unconfirmedSecrets), }; this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore)); } catch (e) { throw e; } } decodeValue(value: number[]): any | null { try { return this.sdkClient.decode_value(value); } catch (e) { secureLogger.error('Failed to decode value: ${e}', { component: 'Service' }); return null; } } async decryptAttribute( processId: string, state: ProcessState, attribute: string ): Promise { secureLogger.info('[decryptAttribute] Starting decryption for attribute: ${attribute}, processId: ${processId}', { component: 'Service' }); let hash = state.pcd_commitment[attribute]; if (!hash) { secureLogger.info('[decryptAttribute] No hash found for attribute: ${attribute}', { component: 'Service' }); return null; } let key = state.keys[attribute]; secureLogger.info('[decryptAttribute] Initial key state for ${attribute}:', { component: 'Service', data: key ? 'present' : 'missing' }); const pairingProcessId = this.getPairingProcessId(); // If key is missing, request an update and then retry if (!key) { const roles = state.roles; let hasAccess = false; // If we're not supposed to have access to this attribute, ignore for (const role of Object.values(roles)) { for (const rule of Object.values(role.validation_rules)) { if (rule.fields.includes(attribute)) { if (role.members.includes(pairingProcessId)) { // We have access to this attribute hasAccess = true; break; } } } } if (!hasAccess) { secureLogger.info('[decryptAttribute] No access rights for attribute: ${attribute}', { component: 'Service' }); return null; } secureLogger.info('[decryptAttribute] Requesting key update for attribute: ${attribute}', { component: 'Service' }); await this.checkConnections((await this.getProcess(processId))!); // We should have the key, so we're going to ask other members for it await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); const maxRetries = 1; const retryDelay = 100; // delay in milliseconds let retries = 0; while ((!hash || !key) && retries < maxRetries) { secureLogger.info('[decryptAttribute] Retry ${retries + 1}/${maxRetries} for attribute: ${attribute}', { component: 'Service' }); await new Promise(resolve => setTimeout(resolve, retryDelay)); // Re-read hash and key after waiting hash = state.pcd_commitment[attribute]; key = state.keys[attribute]; retries++; secureLogger.info('[decryptAttribute] After retry ${retries}: hash=${!!hash}, key=${!!key}', { component: 'Service' }); } } if (hash && key) { secureLogger.info('[decryptAttribute] Starting decryption process with hash: ${hash.substring(0, 8)}...', { component: 'Service' }); const blob = await this.getBlobFromDb(hash); if (blob) { secureLogger.info('[decryptAttribute] Blob retrieved successfully for ${attribute}', { component: 'Service' }); // Decrypt the data const buf = await blob.arrayBuffer(); const cipher = new Uint8Array(buf); const keyUIntArray = this.hexToUInt8Array(key); try { const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher); if (clear) { // deserialize the result to get the actual data const decoded = this.sdkClient.decode_value(clear); return decoded; } else { throw new Error('decrypt_data returned null'); } } catch (e) { secureLogger.error('[decryptAttribute] Failed to decrypt data for ${attribute}:', e, { component: 'Service' }); } } } return null; } getNotifications(): any[] | null { // return [ // { // id: 1, // title: 'Notif 1', // description: 'A normal notification', // sendToNotificationPage: false, // path: '/notif1', // }, // { // id: 2, // title: 'Notif 2', // description: 'A normal notification', // sendToNotificationPage: false, // path: '/notif2', // }, // { // id: 3, // title: 'Notif 3', // description: 'A normal notification', // sendToNotificationPage: false, // path: '/notif3', // }, // ]; return this.notifications; } setNotifications(notifications: any[]) { this.notifications = notifications; } async importJSON(backup: BackUp): Promise { const device = backup.device; // Reset current device await this.resetDevice(); await this.saveDeviceInDatabase(device); this.restoreDevice(device); // TODO restore secrets and processes from file const secretsStore = backup.secrets; await this.restoreSecretsFromBackUp(secretsStore); const processes = backup.processes; await this.restoreProcessesFromBackUp(processes); } public async createBackUp(): Promise { // Get the device from indexedDB const device = await this.getDeviceFromDatabase(); if (!device) { secureLogger.error('No device loaded', { component: 'Service' }); return null; } // Get the processes const processes = await this.getProcesses(); // Get the shared secrets const secrets = await this.getAllSecrets(); // Create a backup object const backUp = { device: device, secrets: secrets, processes: processes, }; return backUp; } // Device 1 wait Device 2 public device1: boolean = false; public device2Ready: boolean = false; public resetState() { this.device1 = false; this.device2Ready = false; } // Handle the handshake message public async handleHandshakeMsg(url: string, parsedMsg: any) { try { // parsedMsg is already parsed by the validator, no need to JSON.parse again const handshakeMsg: HandshakeMessage = parsedMsg; secureLogger.debug('🔍 DEBUG: Handshake message received:', { component: 'Service', data: { url, hasSpAddress: !!handshakeMsg.sp_address, spAddress: handshakeMsg.sp_address, spAddressType: typeof handshakeMsg.sp_address, spAddressLength: handshakeMsg.sp_address?.length } }); if (handshakeMsg.sp_address) { this.updateRelay(url, handshakeMsg.sp_address); this.relayAddresses[url] = handshakeMsg.sp_address; this.resolveRelayReady(); } else { secureLogger.warn('⚠️ Handshake received but sp_address is empty or undefined', { component: 'Service' }); } secureLogger.info('handshakeMsg:', { component: 'Service', data: handshakeMsg }); this.currentBlockHeight = handshakeMsg.chain_tip; secureLogger.info('this.currentBlockHeight:', { component: 'Service', data: this.currentBlockHeight }); this.updateDeviceBlockHeight(); if (this.membersList && Object.keys(this.membersList).length === 0) { // We start from an empty list, just copy it over this.membersList = handshakeMsg.peers_list; } else { // We are incrementing our list for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) { this.membersList[processId] = member as Member; } } setTimeout(async () => { const newProcesses: OutPointProcessMap = handshakeMsg.processes_list; if (!newProcesses || Object.keys(newProcesses).length === 0) { secureLogger.debug('Received empty processes list from', { component: 'Service', data: url }); return; } // Add a flag to prevent processing the same handshake multiple times const handshakeKey = `${url}_${JSON.stringify(handshakeMsg.processes_list)}`; if (this.processedHandshakes && this.processedHandshakes.has(handshakeKey)) { secureLogger.debug('Handshake already processed for', { component: 'Service', data: url }); return; } if (!this.processedHandshakes) { this.processedHandshakes = new Set(); } this.processedHandshakes.add(handshakeKey); if (this.processesCache && Object.keys(this.processesCache).length === 0) { // We restored db but cache is empty, meaning we're starting from scratch try { await this.batchSaveProcessesToDb(newProcesses); } catch (e) { secureLogger.error('Failed to save processes to db:', e, { component: 'Service' }); } } else { // We need to update our processes with what relay provides const toSave: Record = {}; for (const [processId, process] of Object.entries(newProcesses)) { const existing = await this.getProcess(processId); if (existing) { // Look for state id we don't know yet const newStates: string[] = []; const newRoles: Record[] = []; if (!Array.isArray(process.states)) { secureLogger.warn('process.states is not an array:', { component: 'Service', data: process.states }); continue; } for (const state of process.states) { if (!state?.state_id) { continue; } // shouldn't happen if (state.state_id === EMPTY32BYTES) { // We check that the tip is the same we have, if not we update const existingTip = existing.states[existing.states.length - 1].commited_in; if (existingTip !== state.commited_in) { secureLogger.info('Found new tip for process', { component: 'Service', data: processId }); existing.states.pop(); // We discard the last state existing.states.push(state); // We know that's the last state, so we just trigger the update toSave[processId] = existing; } } else if (!this.lookForStateId(existing, state.state_id)) { // We don't want to overwrite what we already have for existing processes // We may end up overwriting the keys for example // So the process we're going to save needs to merge new states with what we already have const existingLastState = existing.states.pop(); if (!existingLastState) { // This should never happen secureLogger.error('Failed to get last state for process', processId, { component: 'Service' }); break; } existing.states.push(state); existing.states.push(existingLastState); toSave[processId] = existing; // We mark it for update if (this.rolesContainsUs(state.roles)) { newStates.push(state.state_id); newRoles.push(state.roles); } } else { // We already have the state, but we check if we have the keys const existingState = this.getStateFromId(existing, state.state_id); if (existingState!.keys && Object.keys(existingState!.keys).length != 0) { // We have some keys, so we just assume everything ok and move on for now continue; } else { // We verify we are part of the roles const roles = state.roles; if (this.rolesContainsUs(roles)) { // We don't have keys, but we are part of the roles, so we need to request the keys // that may also be because we are part of a role that don't have any fields // It's possible but let's request for nothing anyway newStates.push(state.state_id); newRoles.push(roles); } else { // We are simply not involved, move on continue; } } } } if (newStates.length != 0) { await this.checkConnections(existing); await this.requestDataFromPeers(processId, newStates, newRoles); } // Otherwise we're probably just in the initial loading at page initialization } else { // We add it to db toSave[processId] = process; } } if (toSave && Object.keys(toSave).length > 0) { secureLogger.info('batch saving processes to db', { component: 'Service', data: toSave }); await this.batchSaveProcessesToDb(toSave); } } }, 500); } catch (e) { secureLogger.error('Failed to parse init message:', e, { component: 'Service' }); } } private lookForStateId(process: Process, stateId: string): boolean { for (const state of process.states) { if (state.state_id === stateId) { return true; } } return false; } /** * Waits for at least one handshake message to be received from any connected relay. * This ensures that the relay addresses are fully populated and the member list is updated. * @returns A promise that resolves when at least one handshake message is received. */ private async waitForHandshakeMessage(timeoutMs: number = 3000): Promise { const startTime = Date.now(); const pollInterval = 100; // Check every 100ms return new Promise((resolve, reject) => { const checkForHandshake = () => { // Check if we have any members or any relays (indicating handshake was received) if ( Object.keys(this.membersList).length > 0 || Object.keys(this.relayAddresses).length > 0 ) { secureLogger.info('Handshake message received (members or relays present)', { component: 'Service' }); resolve(); return; } // Check timeout if (Date.now() - startTime >= timeoutMs) { reject(new Error(`No handshake message received after ${timeoutMs}ms timeout`)); return; } // Continue polling setTimeout(checkForHandshake, pollInterval); }; checkForHandshake(); }); } /** * Retourne la liste de tous les membres ordonnés par leur process id * @returns Un tableau contenant tous les membres */ public getAllMembersSorted(): Record { return Object.fromEntries( Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) ); } public getAllMembers(): Record { return this.membersList; } public getAddressesForMemberId(memberId: string): string[] | null { try { return this.membersList[memberId].sp_addresses; } catch (e) { return null; } } public compareMembers(memberA: string[], memberB: string[]): boolean { if (!memberA || !memberB) { return false; } if (memberA.length !== memberB.length) { return false; } const res = memberA.every(item => memberB.includes(item)) && memberB.every(item => memberA.includes(item)); return res; } public async handleCommitError(response: string) { const content = JSON.parse(response); const error = content.error; const errorMsg = error['GenericError']; const dontRetry = [ 'State is identical to the previous state', 'Not enough valid proofs', 'Not enough members to validate', ]; if (dontRetry.includes(errorMsg)) { return; } // Wait and retry setTimeout(async () => { this.sendCommitMessage(JSON.stringify(content)); }, 1000); } public getRoles(process: Process): Record | null { const lastCommitedState = this.getLastCommitedState(process); if ( lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0 ) { return lastCommitedState!.roles; } else if (process.states.length === 2) { const firstState = process.states[0]; if (firstState && firstState.roles && Object.keys(firstState.roles).length != 0) { return firstState!.roles; } } return null; } public getPublicData(process: Process): Record | null { const lastCommitedState = this.getLastCommitedState(process); if ( lastCommitedState && lastCommitedState.public_data && Object.keys(lastCommitedState.public_data).length != 0 ) { return lastCommitedState!.public_data; } else if (process.states.length === 2) { const firstState = process.states[0]; if (firstState && firstState.public_data && Object.keys(firstState.public_data).length != 0) { return firstState!.public_data; } } return null; } public getProcessName(process: Process): string | null { const lastCommitedState = this.getLastCommitedState(process); if (lastCommitedState && lastCommitedState.public_data) { const processName = lastCommitedState!.public_data['processName']; if (processName) { return this.decodeValue(processName); } else { return null; } } else { return null; } } public async getMyProcesses(): Promise { // If we're not paired yet, just skip it let pairingProcessId = null; try { pairingProcessId = this.getPairingProcessId(); } catch (e) { return null; } if (!pairingProcessId) { return null; } try { const processes = await this.getProcesses(); const newMyProcesses = new Set(this.myProcesses || []); // MyProcesses automatically contains pairing process newMyProcesses.add(pairingProcessId); for (const [processId, process] of Object.entries(processes)) { // We use myProcesses attribute to not reevaluate all processes everytime if (newMyProcesses.has(processId)) { continue; } try { const roles = this.getRoles(process); if (roles && this.rolesContainsUs(roles)) { newMyProcesses.add(processId); } } catch (e) { secureLogger.error('Error in operation', e as Error, { component: 'Service' }); } } this.myProcesses = newMyProcesses; // atomic update return Array.from(this.myProcesses); } catch (e) { secureLogger.error('Failed to get processes:', e, { component: 'Service' }); return null; } } public async requestDataFromPeers( processId: string, stateIds: string[], roles: Record[] ) { secureLogger.info('Requesting data from peers', { component: 'Service' }); const membersList = Object.values(this.getAllMembers()).map(member => ({ sp_addresses: member.sp_addresses })); try { // Convert objects to strings for WASM compatibility const rolesString = JSON.stringify(roles); const membersString = JSON.stringify(membersList); const stateIdsString = JSON.stringify(stateIds); const res = this.sdkClient.request_data(processId, stateIdsString, rolesString, membersString); await this.handleApiReturn(res); } catch (e) { secureLogger.error('Error requesting data from peers:', e, { component: 'Service' }); throw e; } } public hexToBlob(hexString: string): Blob { const uint8Array = this.hexToUInt8Array(hexString); return new Blob([new Uint8Array(uint8Array)], { type: 'application/octet-stream' }); } public hexToUInt8Array(hexString: string): Uint8Array { if (hexString.length % 2 !== 0) { throw new Error('Invalid hex string: length must be even'); } const uint8Array = new Uint8Array(hexString.length / 2); for (let i = 0; i < hexString.length; i += 2) { uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16); } return uint8Array; } public async blobToHex(blob: Blob): Promise { const buffer = await blob.arrayBuffer(); const bytes = new Uint8Array(buffer); return Array.from(bytes) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); } public getHashForFile( commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array } ): string { return this.sdkClient.hash_value(fileBlob, commitedIn, label); } public getMerkleProofForFile( processState: ProcessState, attributeName: string ): MerkleProofResult { return this.sdkClient.get_merkle_proof(processState, attributeName); } public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean { try { return this.sdkClient.validate_merkle_proof(proof, hash); } catch (e) { throw new Error(`Failed to validate merkle proof: ${e}`); } } public getLastCommitedState(process: Process): ProcessState | null { if (process.states.length === 0) {return null;} const processTip = process.states[process.states.length - 1].commited_in; const lastCommitedState = process.states.findLast(state => state.commited_in !== processTip); if (lastCommitedState) { return lastCommitedState; } else { return null; } } public getLastCommitedStateIndex(process: Process): number | null { if (process.states.length === 0) {return null;} const processTip = process.states[process.states.length - 1].commited_in; for (let i = process.states.length - 1; i >= 0; i--) { if (process.states[i].commited_in !== processTip) { return i; } } return null; } public getUncommitedStates(process: Process): ProcessState[] { if (process.states.length === 0) {return [];} const processTip = process.states[process.states.length - 1].commited_in; const res = process.states.filter(state => state.commited_in === processTip); return res.filter(state => state.state_id !== EMPTY32BYTES); } public getStateFromId(process: Process, stateId: string): ProcessState | null { if (process.states.length === 0) {return null;} const state = process.states.find(state => state.state_id === stateId); if (state) { return state; } else { return null; } } public getNextStateAfterId(process: Process, stateId: string): ProcessState | null { if (process.states.length === 0) {return null;} const index = process.states.findIndex(state => state.state_id === stateId); if (index !== -1 && index < process.states.length - 1) { return process.states[index + 1]; } return null; } public isPairingProcess(roles: Record): boolean { if (Object.keys(roles).length != 1) { return false; } const pairingRole = roles['pairing']; if (pairingRole) { // For now that's enough, we should probably test more things return true; } else { return false; } } public async updateMemberPublicName(process: Process, newName: string): Promise { const publicData = { memberPublicName: newName, }; return await this.updateProcess(process, {}, publicData, null); } }