// @ts-nocheck import { INotification } from '~/models/notification.model'; import { IProcess } from '~/models/process.model'; import { initWebsocket, sendMessage } from '../websockets'; 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, testData } from './storage.service'; import { BackUp } from '~/models/backup.model'; 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; 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 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 secretsAreCompromised: boolean = false; // 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(); await instance.init(); instance.routingInstance = await ModalService.getInstance(); return instance; })(); } console.log('[Services] ⏳ Initialisation des services...'); Services.instance = await Services.initializing; Services.initializing = null; // Reset for potential future use console.log('[Services] ✅ Services initialisés.'); return Services.instance; } public async init(): Promise { this.notifications = this.getNotifications(); this.sdkClient = await import('../../pkg/sdk_client'); this.sdkClient.setup(); for (const wsurl of Object.values(BOOTSTRAPURL)) { this.updateRelay(wsurl, ''); } } public setProcessId(processId: string | null) { this.processId = processId; } 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 connectedUrls: string[] = []; // Connect to all relays for (const wsurl of Object.keys(this.relayAddresses)) { try { console.log(`[Services:connectAllRelays] 🔌 Connexion à: ${wsurl}`); await this.addWebsocketConnection(wsurl); connectedUrls.push(wsurl); console.log(`[Services:connectAllRelays] ✅ Connecté avec succès à: ${wsurl}`); } catch (error) { console.error(`[Services:connectAllRelays] ❌ Échec de la connexion à ${wsurl}:`, error); } } // Wait for at least one handshake message if we have connections if (connectedUrls.length > 0) { try { await this.waitForHandshakeMessage(); } catch (e) { console.error(`[Services:connectAllRelays] ⌛️ ${e.message}`); } } } private getRelayReadyPromise(): Promise { if (!this.relayReadyPromise) { this.relayReadyPromise = new Promise((resolve) => { this.relayReadyResolver = resolve; }); } return this.relayReadyPromise; } private resolveRelayReady(): void { if (this.relayReadyResolver) { this.relayReadyResolver(); this.relayReadyResolver = null; this.relayReadyPromise = null; } } public async addWebsocketConnection(url: string): Promise { console.log("[Services:addWebsocketConnection] 🕸️ Ouverture d'une nouvelle connexion websocket..."); 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) { console.log(`[Services:updateRelay] ✅ Mise à jour du relais ${url} avec spAddress ${spAddress}`); 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 { console.log('[Services:printAllRelays] Adresses relais actuelles:'); for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) { console.log(`${wsurl} -> ${spAddress}`); } } public isPaired(): boolean { try { const result = this.sdkClient.is_paired(); return result; } catch (e) { throw new Error(`[Services:isPaired] Erreur: ${e}`); } } public async unpairDevice(): Promise { try { console.log("[Services:unpairDevice] 🚫 Dissociation de l'appareil..."); this.sdkClient.unpair_device(); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); console.log('[Services:unpairDevice] ✅ Appareil dissocié et sauvegardé.'); } catch (e) { throw new Error(`[Services:unpairDevice] Échec de la dissociation: ${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'); } /** * Ensure that the in-memory members list is populated. * If empty, (re)connect to relays and wait for a handshake to fill it. */ public async ensureMembersAvailable(): Promise { try { if (Object.keys(this.membersList).length > 0) { // console.debug('[Services:ensureMembersAvailable] ✅ Liste des membres déjà disponible.'); return; } console.warn('[Services:ensureMembersAvailable] ⚠️ Liste des membres vide. Tentative de connexion aux relais...'); // Attempt to connect to relays and wait for handshake which updates membersList await this.connectAllRelays(); console.log(`[Services:ensureMembersAvailable] ✅ Connexion aux relais terminée. ${Object.keys(this.membersList).length} membres chargés.`); } catch (e) { console.error('[Services:ensureMembersAvailable] ❌ Échec de la récupération des membres:', e); } } public async getDiffByValue(value: string): Promise { const db = await Database.getInstance(); const store = 'diffs'; const res = await db.getObject(store, value); return res; } private async getTokensFromFaucet(): Promise { console.log('[Services:getTokensFromFaucet] 🚰 Demande de tokens au faucet...'); try { await this.ensureSufficientAmount(); } catch (e) { console.error('[Services:getTokensFromFaucet] ❌ Échec, vérifiez la connexion au relais.'); return; } } /** * Tente d'établir des connexions (secrets partagés) avec les membres d'un état de processus. */ public async ensureConnections(process: Process, stateId: string | null = null): Promise { const processId = process?.process_id; // Utilisation de l'optional chaining au cas où process est null console.info(`[ConnectionCheck] 🔄 Démarrage de la vérification des connexions pour le processus ${processId} (StateID: ${stateId || 'par défaut'})`); if (!process) { console.error(`[ConnectionCheck] 💥 ERREUR CRITIQUE: ensureConnections a été appelée avec un processus nul ou undefined.`); return; } // 1. Déterminer quel état analyser const state = this.getStateToCheck(process, stateId); if (!state) { console.warn(`[ConnectionCheck] ⚠️ Aucun état valide trouvé pour le processus ${processId}. (States: ${process.states.length}, StateID demandé: ${stateId}). Abandon.`); return; } // 2. Tenter de trouver les membres dans les rôles de cet état // --- AMÉLIORATION: Appel 'await' ajouté pour corriger la race condition --- let members = await this.getMembersFromState(state); if (members.size === 0) { console.log(`[ConnectionCheck] ℹ️ Aucun membre trouvé dans les rôles. Vérification s'il s'agit d'un processus de pairing...`); members = this.getPairingMembers(process); // Tente la logique de pairing } if (members.size === 0) { console.log(`[ConnectionCheck] 🏁 Aucun membre (rôles ou pairing) trouvé à qui se connecter. Tâche terminée.`); return; } // 3. Trouver les membres auxquels nous ne sommes pas encore connectés const unconnectedAddresses = await this.findUnconnectedAddresses(members); if (unconnectedAddresses.size === 0) { console.log(`[ConnectionCheck] ✅ Déjà connecté aux ${members.size} membre(s) trouvés.`); return; } // 4. Se connecter aux membres manquants console.log(`[ConnectionCheck] 📡 ${unconnectedAddresses.size} adresse(s) non connectée(s) trouvée(s). Tentative de connexion...`, Array.from(unconnectedAddresses)); // getTokensFromFaucet() est maintenant géré DANS connectAddresses try { const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses)); if (apiResult) { console.log(`[ConnectionCheck] 🎁 Réponse de 'connectAddresses' reçue, transfert à handleApiReturn...`); await this.handleApiReturn(apiResult); } else { console.log(`[ConnectionCheck] 🤷 'connectAddresses' n'a renvoyé aucun résultat (peut-être un 409 Conflict géré).`); } } catch (error) { console.error(`[ConnectionCheck] 💥 Échec lors de l'appel à connectAddresses: ${error}`, error); } } // --- FONCTIONS D'AIDE (à placer dans la même classe) --- /** * Helper pour obtenir l'état de processus pertinent à vérifier. * La logique par défaut (si stateId est nul) est de prendre l'avant-dernier état. */ private getStateToCheck(process: Process, stateId: string | null): ProcessState | null { if (stateId) { const state = process.states.find((s) => s.state_id === stateId); if (!state) { console.warn(`[ConnectionCheck] ⚠️ Impossible de trouver l'état avec l'ID: ${stateId}`); return null; } return state; } // Logique par défaut: prendre l'avant-dernier état (nécessite au moins 2 états) if (process.states.length < 2) { console.warn(`[ConnectionCheck] ⚠️ Logique par défaut requiert 2 états, mais seulement ${process.states.length} trouvé(s).`); return null; } // AMÉLIORATION: Log pour cette logique fragile console.debug(`[ConnectionCheck] ℹ️ Utilisation de l'état n°${process.states.length - 2} (l'avant-dernier) comme état par défaut.`); return process.states[process.states.length - 2]; } /** * Helper pour extraire les membres des rôles d'un état. * --- AMÉLIORATION: Devenu 'async' pour corriger une race condition --- */ private async getMembersFromState(state: ProcessState): Promise> { await this.ensureMembersAvailable(); // S'ASSURE que membersList est chargé const members = new Set(); if (!state.roles) { console.warn(`[ConnectionCheck] ⚠️ L'état ${state.state_id} n'a pas de propriété 'roles'.`); return members; } for (const role of Object.values(state.roles)) { for (const memberId of role.members) { const memberAddresses = this.getAddressesForMemberId(memberId); if (memberAddresses && memberAddresses.length > 0) { members.add({ sp_addresses: memberAddresses }); } else { console.warn(`[ConnectionCheck] ⚠️ Impossible de trouver les adresses pour le membre ${memberId} (présent dans les rôles).`); } } } return members; } /** * Helper pour la logique spécifique de "pairing" : * cherche 'pairedAddresses' dans l'historique du processus. */ private getPairingMembers(process: Process): Set { const members = new Set(); let publicData: Record | null = null; // Cherche 'pairedAddresses' en remontant l'historique 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; console.log(`[ConnectionCheck] ℹ️ 'pairedAddresses' trouvé dans l'état ${i} (state_id: ${state.state_id})`); break; } } if (publicData && publicData['pairedAddresses']) { const decodedAddresses = this.decodeValue(publicData['pairedAddresses']); if (decodedAddresses && decodedAddresses.length > 0) { members.add({ sp_addresses: decodedAddresses }); } else { console.warn(`[ConnectionCheck] ⚠️ 'pairedAddresses' trouvé mais vide après décodage.`); } } return members; } /** * Helper pour filtrer une liste de membres et ne garder que ceux * pour qui nous n'avons pas de secret local. */ private async findUnconnectedAddresses(members: Set): Promise> { const unconnected = 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; if (this.secretsAreCompromised) { console.warn(`[findUnconnectedAddresses] 🚩 Flag 'secretsAreCompromised' détecté. Forçage de la reconnexion pour ${address}.`); unconnected.add(address); continue; // Important: passe au membre suivant } for (const address of sp_addresses) { if (address === myAddress) continue; // On s'ignore soi-même if ((await this.getSecretForAddress(address)) === null) { unconnected.add(address); } } } if (this.secretsAreCompromised && unconnected.size > 0) { console.log("[findUnconnectedAddresses] 🚩 Drapeau 'secretsAreCompromised' réinitialisé car une reconnexion va être tentée."); this.secretsAreCompromised = false; } return unconnected; } // --- AMÉLIORATION: Ajout de la logique "try-catch-retry" du faucet --- public async connectAddresses(addresses: string[]): Promise { if (addresses.length === 0) { console.warn("[Services:connectAddresses] Appel avec une liste d'adresses vide."); return null; } const feeRate = 1; // Devrait être un paramètre ? try { // 1. Première tentative console.log(`[Services:connectAddresses] 💬 Tentative de connexion (create_transaction) à ${addresses.length} adresse(s).`); return this.sdkClient.create_transaction(addresses, feeRate); } catch (error) { // 2. Vérifier si c'est *exactement* l'erreur de fonds if (this.isInsufficientFundsError(error)) { console.warn('[Services:connectAddresses] 💰 Fonds insuffisants détectés. Appel du faucet pour recharger...'); try { // 3. Appel au faucet (le "remède") await this.getTokensFromFaucet(); // On recharge // 4. Seconde (et dernière) tentative console.log('[Services:connectAddresses] 💬 Nouvelle tentative de connexion post-recharge...'); return this.sdkClient.create_transaction(addresses, feeRate); } catch (retryError) { console.error('[Services:connectAddresses] 💥 Échec critique : Impossible de se connecter, même après recharge.', retryError); throw new Error("Le système n'a pas pu financer la connexion. Échec."); } } else { // 5. Ce n'était pas une erreur de fonds. console.error(`[Services:connectAddresses] 💥 Erreur non liée aux fonds lors de la connexion: ${error}`, error); throw error; // Relancer l'erreur originale } } } private async ensureSufficientAmount(): Promise { const availableAmt = this.getAmount(); const target: BigInt = DEFAULTAMOUNT * BigInt(10); if (availableAmt < target) { console.log(`[Services:ensureSufficientAmount] 💵 Montant insuffisant (${availableAmt}). Demande au faucet...`); const faucetMsg = this.createFaucetMessage(); this.sendFaucetMessage(faucetMsg); await this.waitForAmount(target); console.log(`[Services:ensureSufficientAmount] ✅ Montant suffisant atteint.`); } } private async waitForAmount(target: BigInt): Promise { let attempts = 3; while (attempts > 0) { const amount = this.getAmount(); if (amount >= target) { return amount; } console.log(`[Services:waitForAmount] ⏳ Attente de fonds... Tentative ${4 - attempts}/3`); attempts--; if (attempts > 0) { await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second } } throw new Error('Le montant est toujours 0 après 3 tentatives'); } public async createPairingProcess(userName: string, pairWith: string[]): Promise { console.log("[Services:createPairingProcess] 🤝 Création d'un processus de pairing..."); if (this.sdkClient.is_paired()) { throw new Error("L'appareil est déjà appairé"); } 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(`[Services:createPairingProcess] 💥 Échec: ${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 }; } // --- AMÉLIORATION: Logique de 'ensureConnections' déplacée ici --- public async createProcess(privateData: Record, publicData: Record, roles: Record, feeRate: number = 1): Promise { console.log("[Services:createProcess] 📝 Création d'un nouveau processus..."); const relayAddress = await this.getAvailableRelayAddress(); const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); const members = this.getAllMembers(); try { // 1. Première tentative const result = await this.attemptProcessCreation(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members); // --- AMÉLIORATION: Déplacé ici depuis le 'router' --- // On s'assure qu'on est connecté aux membres du processus qu'on vient de créer. console.log(`[Services:createProcess] 📞 Vérification des connexions pour le nouveau processus ${result.updated_process.process_id}`); await this.ensureConnections(result.updated_process.current_process); return result; } catch (error) { // 2. Vérifier si c'est *exactement* l'erreur de fonds if (this.isInsufficientFundsError(error)) { console.warn('[Services:createProcess] 💰 Fonds insuffisants détectés. Appel du faucet pour recharger...'); try { // 3. Appel au faucet await this.getTokensFromFaucet(); // On recharge // 4. Seconde (et dernière) tentative console.log('[Services:createProcess] 🔄 Nouvelle tentative de création de processus post-recharge...'); const result = await this.attemptProcessCreation(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members); // --- AMÉLIORATION: Déplacé ici depuis le 'router' --- console.log(`[Services:createProcess] 📞 Vérification des connexions pour le nouveau processus ${result.updated_process.process_id} (après retry)`); await this.ensureConnections(result.updated_process.current_process); return result; } catch (retryError) { console.error('[Services:createProcess] 💥 Échec critique : Impossible de créer le processus, même après recharge.', retryError); throw new Error("Le système n'a pas pu financer l'opération. Échec de la création."); } } else { // 5. Ce n'était pas une erreur de fonds. console.error('[Services:createProcess] 💥 Erreur non liée aux fonds lors de la création du processus:', error); throw error; // Relancer l'erreur originale } } } /** * Encapsule l'appel au SDK pour le réutiliser (tentative 1 et 2). */ private async attemptProcessCreation(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members): Promise { console.log('[Services:attemptProcessCreation] 📦 Appel de sdkClient.create_new_process...'); const result = this.sdkClient.create_new_process(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members); if (result.updated_process) { console.log('[Services:attemptProcessCreation] ✅ Processus créé avec succès:', result.updated_process.process_id); return result; } else { throw new Error("[Services:attemptProcessCreation] 💥 sdkClient.create_new_process n'a renvoyé aucun processus mais n'a pas levé d'erreur."); } } /** * Vérifie de manière robuste si l'erreur est bien celle des fonds insuffisants. */ private isInsufficientFundsError(error: any): boolean { const errorString = String(error.message || error.error || error); return errorString.includes('Insufficient funds'); } /** * Tente d'obtenir une adresse de relais, en attendant si nécessaire. */ private async getAvailableRelayAddress(): Promise { let relayAddress = this.getAllRelays()[0]?.spAddress; // TODO: Améliorer la sélection if (!relayAddress) { console.log('[Services:getAvailableRelayAddress] ⏳ Aucun relais prêt. En attente du handshake...'); await this.getRelayReadyPromise(); relayAddress = this.getAllRelays()[0]?.spAddress; } if (!relayAddress) { throw new Error('[Services:getAvailableRelayAddress] ❌ Aucune adresse de relais disponible après attente'); } return relayAddress; } /** * Sépare et encode les données JSON et binaires. */ private async prepareProcessData(privateData: any, publicData: any): Promise<{ encodedPrivateData: any; encodedPublicData: any }> { // TODO: Exécuter l'encodage lourd dans un Web Worker 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), }; return { encodedPrivateData, encodedPublicData }; } public async updateProcess(process: Process, privateData: Record, publicData: Record, roles: Record | null): Promise { console.log(`[Services:updateProcess] 🔄 Mise à jour du processus ${process.process_id}...`); // If roles is null, we just take the last commited state roles if (!roles) { roles = this.getRoles(process); } else { console.log('[Services:updateProcess] ℹ️ Utilisation de nouveaux rôles fournis:', 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 result = this.sdkClient.update_process(process, encodedPrivateData, roles, encodedPublicData, this.getAllMembers()); if (result.updated_process) { console.log(`[Services:updateProcess] ✅ Processus ${process.process_id} mis à jour. Vérification des connexions...`); await this.ensureConnections(result.updated_process.current_process); return result; } else { throw new Error('[Services:updateProcess] 💥 updated_process vide dans updateProcessReturn'); } } catch (e) { throw new Error(`[Services:updateProcess] 💥 Échec: ${e}`); } } public async createPrdUpdate(processId: string, stateId: string): Promise { console.log(`[Services:createPrdUpdate] 📤 Création d'une mise à jour PRD pour ${processId}:${stateId}`); const process = await this.getProcess(processId); if (!process) { throw new Error('[Services:createPrdUpdate] 💥 Processus inconnu'); } else { await this.ensureConnections(process); } try { return this.sdkClient.create_update_message(process, stateId, this.getAllMembers()); } catch (e) { throw new Error(`[Services:createPrdUpdate] 💥 Échec: ${e}`); } } public async createPrdResponse(processId: string, stateId: string): Promise { console.log(`[Services:createPrdResponse] 📥 Création d'une réponse PRD pour ${processId}:${stateId}`); const process = await this.getProcess(processId); if (!process) { throw new Error('[Services:createPrdResponse] 💥 Processus inconnu'); } try { return this.sdkClient.create_response_prd(process, stateId, this.getAllMembers()); } catch (e) { throw new Error(`[Services:createPrdResponse] 💥 Échec: ${e}`); } } public async approveChange(processId: string, stateId: string): Promise { console.log(`[Services:approveChange] 👍 Approbation du changement ${processId}:${stateId}`); const process = await this.getProcess(processId); if (!process) { throw new Error("[Services:approveChange] 💥 Échec de l'obtention du processus depuis la BDD"); } try { const result = this.sdkClient.validate_state(process, stateId, this.getAllMembers()); if (result.updated_process) { await this.ensureConnections(result.updated_process.current_process); return result; } else { throw new Error('[Services:approveChange] 💥 updated_process vide dans approveChangeReturn'); } } catch (e) { throw new Error(`[Services:approveChange] 💥 Échec: ${e}`); } } public async rejectChange(processId: string, stateId: string): Promise { console.log(`[Services:rejectChange] 👎 Rejet du changement ${processId}:${stateId}`); const process = await this.getProcess(processId); if (!process) { throw new Error("[Services:rejectChange] 💥 Échec de l'obtention du processus depuis la BDD"); } try { return this.sdkClient.refuse_state(process, stateId); } catch (e) { throw new Error(`[Services:rejectChange] 💥 Échec: ${e}`); } } async resetDevice() { console.warn("[Services:resetDevice] ⚠️ RÉINITIALISATION COMPLÈTE de l'appareil et de la BDD..."); this.sdkClient.reset_device(); // Clear all stores const db = await Database.getInstance(); await db.clearStore('wallet'); await db.clearStore('shared_secrets'); await db.clearStore('unconfirmed_secrets'); await db.clearStore('processes'); await db.clearStore('diffs'); console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.'); } sendNewTxMessage(message: string) { console.log('[Services:sendNewTxMessage] ✉️ Envoi de NewTx...'); sendMessage('NewTx', message); } sendCommitMessage(message: string) { console.log('[Services:sendCommitMessage] ✉️ Envoi de Commit...'); sendMessage('Commit', message); } sendCipherMessages(ciphers: string[]) { console.log(`[Services:sendCipherMessages] ✉️ Envoi de ${ciphers.length} cipher(s)...`); for (let i = 0; i < ciphers.length; i++) { const cipher = ciphers[i]; sendMessage('Cipher', cipher); } } sendFaucetMessage(message: string): void { console.log('[Services:sendFaucetMessage] ✉️ Envoi de Faucet...'); sendMessage('Faucet', message); } // --- AMÉLIORATION: Ajout de la solution "bombe" pour casser la boucle --- async parseCipher(message: string) { const membersList = this.getAllMembers(); const processes = await this.getProcesses(); try { console.debug('[Services:parseCipher] 🤫 Tentative de déchiffrement du message...'); const apiReturn = this.sdkClient.parse_cipher(message, membersList, processes); console.debug('[Services:parseCipher] ✅ Message déchiffré, traitement...'); await this.handleApiReturn(apiReturn); // Si le déchiffrement réussit, c'est que nos secrets sont bons. // On réinitialise le drapeau (au cas où il était levé). if (this.secretsAreCompromised) { console.log("[Services:parseCipher] ✅ Le déchiffrement a réussi. Réinitialisation du drapeau 'secretsAreCompromised'."); this.secretsAreCompromised = false; } } catch (e) { console.error(`[Services:parseCipher] 💥 Échec critique du déchiffrement: ${e}`); console.warn(`[Services:parseCipher] Contrainte d'anonymat: L'expéditeur est inconnu.`); // On ne supprime rien. On lève juste un drapeau pour // forcer 'ensureConnections' à se méfier de la BDD. console.warn(`[Services:parseCipher] 🚩 ACTION: Levée du drapeau 'secretsAreCompromised'.`); this.secretsAreCompromised = true; } } async parseNewTx(newTxMsg: string) { console.log('[Services:parseNewTx] 📄 Nouveau message NewTx reçu.'); const parsedMsg: NewTxMessage = JSON.parse(newTxMsg); if (parsedMsg.error !== null) { console.error('[Services:parseNewTx] 💥 Erreur dans le message NewTx:', parsedMsg.error); return; } const membersList = this.getAllMembers(); // 1. Mettre à jour les processus affectés par cette transaction await this.updateProcessesFromNewTx(parsedMsg.transaction); // 2. Mettre à jour le portefeuille et l'état de l'appareil await this.updateWalletFromNewTx(newTxMsg, membersList); } /** * Sous-fonction de parseNewTx: Met à jour les processus en cache. */ private async updateProcessesFromNewTx(transaction: any) { try { const prevouts = this.sdkClient.get_prevouts(transaction); // console.debug('[Services:updateProcessesFromNewTx] Prevouts de la tx:', 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.process_id; // Utilisation de l'ID stocké const newTip = this.sdkClient.get_txid(transaction); console.log(`[Services:updateProcessesFromNewTx] 🔗 La Tx ${newTip} dépense le tip du processus ${processId}`); const newStateId = this.sdkClient.get_opreturn(transaction); console.log('[Services:updateProcessesFromNewTx] 📄 Nouvel stateId (op_return):', newStateId); const updatedProcess = this.sdkClient.process_commit_new_state(process, newStateId, newTip); this.processesCache[processId] = updatedProcess; console.log('[Services:updateProcessesFromNewTx] ✅ Processus mis à jour en cache:', updatedProcess); break; // On suppose qu'une tx ne met à jour qu'un seul processus } } } catch (e) { console.error("[Services:updateProcessesFromNewTx] 💥 Échec de l'analyse NewTx pour les commitments:", e); } } /** * Sous-fonction de parseNewTx: Met à jour le portefeuille. */ private async updateWalletFromNewTx(newTxMsg: string, membersList: Record) { try { const parsedTx = this.sdkClient.parse_new_tx(newTxMsg, 0, membersList); if (parsedTx && (parsedTx.partial_tx || parsedTx.new_tx_to_send || parsedTx.secrets || parsedTx.updated_process)) { console.log('[Services:updateWalletFromNewTx] ℹ️ La Tx contient des données pertinentes. Traitement par handleApiReturn...'); try { await this.handleApiReturn(parsedTx); const newDevice = this.dumpDeviceFromMemory(); // Preserve pairing_process_commitment from existing device const existingDevice = await this.getDeviceFromDatabase(); if (existingDevice && existingDevice.pairing_process_commitment) { newDevice.pairing_process_commitment = existingDevice.pairing_process_commitment; } await this.saveDeviceInDatabase(newDevice); console.log('[Services:updateWalletFromNewTx] ✅ Appareil mis à jour et sauvegardé.'); } catch (e) { console.error("[Services:updateWalletFromNewTx] 💥 Échec de la mise à jour de l'appareil après NewTx:", e); } } else { // console.debug('[Services:updateWalletFromNewTx] ℹ️ La Tx ne contenait pas de données pertinentes pour le portefeuille.'); } } catch (e) { // C'est souvent normal (ex: une tx qui ne nous concerne pas) // console.debug('[Services:updateWalletFromNewTx] ℹ️ sdkClient.parse_new_tx n\'a rien trouvé:', e); } } // --- AMÉLIORATION: Logs ajoutés --- public async handleApiReturn(apiReturn: ApiReturn) { console.log("[Services:handleApiReturn] 📥 Traitement d'un nouvel objet ApiReturn...", apiReturn); // 1. Validation initiale if (!this.isValidApiReturn(apiReturn)) { console.log('[Services:handleApiReturn] ⏩ ApiReturn vide ou invalide. Skip.'); return; } try { // 2. Gestion de la signature de transaction const newTxFromSigning = apiReturn.partial_tx ? await this.handlePartialTx(apiReturn.partial_tx) : null; // 3. Gestion de l'envoi de transaction const txData = newTxFromSigning || apiReturn.new_tx_to_send; if (txData && txData.transaction.length != 0) { console.log("[Services:handleApiReturn] 📤 Envoi d'une nouvelle transaction..."); await this.handleNewTx(txData); } // 4. Gestion des secrets if (apiReturn.secrets) { console.log('[Services:handleApiReturn] 🔑 Gestion des secrets...'); await this.handleSecrets(apiReturn.secrets); } // 5. Gestion du processus mis à jour if (apiReturn.updated_process) { console.log('[Services:handleApiReturn] 🔄 Gestion de la mise à jour de processus...'); await this.handleUpdatedProcess(apiReturn.updated_process); } // 6. Gestion du push vers le stockage if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) { console.log('[Services:handleApiReturn] ☁️ Poussée de données vers le stockage...'); await this.handlePushToStorage(apiReturn.push_to_storage); } // 7. Gestion du "commit" à envoyer if (apiReturn.commit_to_send) { console.log('[Services:handleApiReturn] 📤 Envoi de Commit...'); this.handleCommit(apiReturn.commit_to_send); } // 8. Gestion des "ciphers" à envoyer if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) { console.log('[Services:handleApiReturn] 📤 Envoi de Ciphers...'); this.handleCiphers(apiReturn.ciphers_to_send); } } catch (error) { console.error('[Services:handleApiReturn] 💥 ERREUR CRITIQUE lors du traitement de ApiReturn:', error); } } private isValidApiReturn(apiReturn: ApiReturn): boolean { if (!apiReturn || Object.keys(apiReturn).length === 0) { return false; } const hasValidData = Object.values(apiReturn).some((value) => value !== null && value !== undefined); return hasValidData; } private async handlePartialTx(partialTx): Promise { console.log("[Services:handlePartialTx] ✍️ Signature d'une transaction partielle..."); try { const res = this.sdkClient.sign_transaction(partialTx); return res.new_tx_to_send; } catch (e) { console.error('[Services:handlePartialTx] 💥 Échec de la signature:', e); return null; } } private async handleNewTx(txData: any) { this.sendNewTxMessage(JSON.stringify(txData)); // 🚨 ATTENTION: C'est un anti-pattern (code smell). // Cette attente arbitraire doit être remplacée par un // véritable mécanisme d'acquittement (par ex. une Promise // retournée par sendNewTxMessage). console.warn('[Services:handleNewTx] ⏳ Attente arbitraire de 500ms...'); await new Promise((r) => setTimeout(r, 500)); } private async handleSecrets(secrets: any) { const { unconfirmed_secrets, shared_secrets } = secrets; const db = await Database.getInstance(); // Sauvegarder les secrets non confirmés if (unconfirmed_secrets && unconfirmed_secrets.length > 0) { console.log(`[Services:handleSecrets] 💾 Sauvegarde de ${unconfirmed_secrets.length} secret(s) non confirmé(s)`); for (const secret of unconfirmed_secrets) { try { await db.addObject({ storeName: 'unconfirmed_secrets', object: secret, key: null, }); } catch (e) { console.error("[Services:handleSecrets] 💥 Échec de sauvegarde d'un secret non confirmé:", e); } } } // Sauvegarder les secrets partagés (confirmés) if (shared_secrets && Object.keys(shared_secrets).length > 0) { const entries = Object.entries(shared_secrets).map(([key, value]) => ({ key, value })); console.log(`[Services:handleSecrets] 💾 Sauvegarde de ${entries.length} secret(s) partagé(s)`); for (const entry of entries) { try { await db.addObject({ storeName: 'shared_secrets', object: entry.value, key: entry.key, }); console.log(`[Services:handleSecrets] ✅ Secret partagé pour ${entry.key} sauvegardé.`); } catch (e) { console.error(`[Services:handleSecrets] 💥 Échec de l'ajout du secret partagé pour ${entry.key}:`, e); } } } } private async handleUpdatedProcess(updatedProcess: any) { const processId: string = updatedProcess.process_id; console.log(`[Services:handleUpdatedProcess] 🔄 Traitement des mises à jour pour le processus ${processId}`); // Sauvegarder les données chiffrées if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) { await this.saveEncryptedData(updatedProcess.encrypted_data); } // Sauvegarder le processus lui-même await this.saveProcessToDb(processId, updatedProcess.current_process); // Sauvegarder les diffs if (updatedProcess.diffs && updatedProcess.diffs.length != 0) { try { await this.saveDiffsToDb(updatedProcess.diffs); } catch (e) { console.error('[Services:handleUpdatedProcess] 💥 Échec de la sauvegarde des diffs:', e); } } // Vérifier la logique métier spécifique au pairing await this.checkAndConfirmPairing(processId, updatedProcess); } private async saveEncryptedData(encryptedData: Record) { console.log(`[Services:saveEncryptedData] 💾 Sauvegarde de ${Object.keys(encryptedData).length} blob(s) chiffré(s)...`); for (const [hash, cipher] of Object.entries(encryptedData)) { const blob = this.hexToBlob(cipher); try { await this.saveBlobToDb(hash, blob); } catch (e) { console.error(`[Services:saveEncryptedData] 💥 Échec de la sauvegarde du blob pour ${hash}:`, e); } } } private async checkAndConfirmPairing(processId: string, updatedProcess: any) { try { const existingDevice = await this.getDeviceFromDatabase(); if (!existingDevice || existingDevice.pairing_process_commitment !== processId) { // console.debug('[Services:checkAndConfirmPairing] ℹ️ Ce n\'est pas le processus de pairing de cet appareil. Skip.'); return; // Ce n'est pas le processus de pairing de cet appareil } // C'est notre processus de pairing, vérifions s'il est prêt const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1]; if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) { console.log('[Services:checkAndConfirmPairing] 🤝 Processus de pairing mis à jour avec les adresses. Confirmation automatique...'); await this.confirmPairing(); } } catch (e) { console.error("[Services:checkAndConfirmPairing] 💥 Échec de l'auto-confirmation du pairing:", e); } } private async handlePushToStorage(hashes: string[]) { console.log(`[Services:handlePushToStorage] ☁️ Demande de push pour ${hashes.length} hash(es)`); for (const hash of hashes) { try { const blob = await this.getBlobFromDb(hash); if (!blob) { console.error(`[Services:handlePushToStorage] 💥 Échec: blob non trouvé en BDD pour le hash ${hash}`); continue; } const diff = await this.getDiffByValueFromDb(hash); if (!diff) { console.error(`[Services:handlePushToStorage] 💥 Échec: diff non trouvé en BDD pour le hash ${hash}`); continue; } const storages = diff.storages; console.log(`[Services:handlePushToStorage] ☁️ Poussée de ${hash} vers ${storages.length} storage(s)...`); await this.saveDataToStorage(storages, hash, blob, null); } catch (e) { console.error(`[Services:handlePushToStorage] 💥 Échec du push pour ${hash}:`, e); } } } private handleCommit(commit: any) { this.sendCommitMessage(JSON.stringify(commit)); } private handleCiphers(ciphers: any[]) { this.sendCipherMessages(ciphers); } public async openPairingConfirmationModal(processId: string) { console.log('[Services:openPairingConfirmationModal] 띄 Ouverture du modal de confirmation...'); const process = await this.getProcess(processId); if (!process) { console.error('[Services:openPairingConfirmationModal] 💥 Échec: processus de pairing non trouvé'); 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) { console.error(e); } } public async confirmPairing() { console.log('[Services:confirmPairing] 🤝 Confirmation du pairing...'); try { // Get the pairing process ID from database const existingDevice = await this.getDeviceFromDatabase(); if (!existingDevice || !existingDevice.pairing_process_commitment) { console.error('[Services:confirmPairing] 💥 Aucun engagement de processus de pairing trouvé'); return; } const pairingProcessId = existingDevice.pairing_process_commitment; console.log(`[Services:confirmPairing] ℹ️ Processus ID: ${pairingProcessId}`); // Get the pairing process to extract paired addresses const myPairingProcess = await this.getProcess(pairingProcessId); if (!myPairingProcess) { console.error('[Services:confirmPairing] 💥 Processus de pairing inconnu'); return; } // Try to get committed state first, fallback to current state let myPairingState = this.getLastCommitedState(myPairingProcess); if (!myPairingState && myPairingProcess.states.length > 0) { // If no committed state, use the current state myPairingState = myPairingProcess.states[myPairingProcess.states.length - 1]; console.log('[Services:confirmPairing] ⚠️ Utilisation de l\'état actuel au lieu de l\'état "commited"'); } if (!myPairingState) { console.error('[Services:confirmPairing] 💥 Aucun état trouvé dans le processus de pairing'); return; } const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; if (!encodedSpAddressList) { console.error("[Services:confirmPairing] 💥 Aucune adresse d'appairage trouvée dans l'état"); return; } const spAddressList = this.decodeValue(encodedSpAddressList); if (spAddressList.length === 0) { console.error('[Services:confirmPairing] 💥 pairedAddresses est vide'); return; } console.log(`[Services:confirmPairing] ℹ️ ${spAddressList.length} adresses trouvées pour l'appairage.`); // ... (Suppression du bloc de test 'test_process_id_parsing' pour la clarté) this.sdkClient.unpair_device(); // Clear any existing pairing try { console.log('[Services:confirmPairing] 📞 Appel de sdkClient.pair_device()...'); this.sdkClient.pair_device(pairingProcessId, spAddressList); console.log('[Services:confirmPairing] ✅ Appel de pair_device() réussi (côté SDK).'); } catch (pairError) { console.error('[Services:confirmPairing] 💥 sdkClient.pair_device() a échoué:', pairError); throw pairError; } // Verify pairing was successful const isPairedAfterPairing = this.sdkClient.is_paired(); console.log('[Services:confirmPairing] ❓ Statut is_paired après appel:', isPairedAfterPairing); // Save the updated device const newDevice = this.dumpDeviceFromMemory(); console.log('[Services:confirmPairing] ℹ️ Appareil en mémoire après appairage:', { pairing_process_commitment: newDevice.pairing_process_commitment, paired_member: newDevice.paired_member, }); // IMPORTANT: Only set pairing_process_commitment if WASM pairing succeeded if (isPairedAfterPairing) { console.log("[Services:confirmPairing] ℹ️ L'appairage WASM a réussi, conservation de l'engagement WASM"); } else { console.warn("[Services:confirmPairing] ⚠️ L'appairage WASM a échoué, définition manuelle de l'engagement (fallback)"); newDevice.pairing_process_commitment = pairingProcessId; } await this.saveDeviceInDatabase(newDevice); // Final verification const finalIsPaired = this.sdkClient.is_paired(); console.log('[Services:confirmPairing] ✅ Statut final is_paired:', finalIsPaired); console.log(`[Services:confirmPairing] ✅ Appareil appairé avec succès au processus: ${pairingProcessId}`); } catch (e) { console.error('[Services:confirmPairing] 💥 Échec global de la confirmation du pairing:', e); return; } } public async updateDevice(): Promise { console.log("[Services:updateDevice] 🔄 Mise à jour de l'appareil..."); let myPairingProcessId: string; try { myPairingProcessId = this.getPairingProcessId(); } catch (e) { console.error("[Services:updateDevice] 💥 Échec de l'obtention du pairing process id"); return; } const myPairingProcess = await this.getProcess(myPairingProcessId); if (!myPairingProcess) { console.error('[Services:updateDevice] 💥 Processus de pairing inconnu'); return; } const myPairingState = this.getLastCommitedState(myPairingProcess); if (myPairingState) { const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; const spAddressList = this.decodeValue(encodedSpAddressList); if (spAddressList.length === 0) { console.error('[Services:updateDevice] 💥 pairedAddresses est vide'); return; } // We can check if our address is included and simply unpair if it's not if (!spAddressList.includes(this.getDeviceAddress())) { console.warn("[Services:updateDevice] ⚠️ Notre adresse n'est plus dans la liste. Dissociation..."); await this.unpairDevice(); return; } // We can update the device with the new addresses console.log("[Services:updateDevice] 🔄 Ré-appairage avec la nouvelle liste d'adresses..."); this.sdkClient.unpair_device(); this.sdkClient.pair_device(myPairingProcessId, spAddressList); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); console.log('[Services:updateDevice] ✅ Appareil mis à jour.'); } } public pairDevice(processId: string, spAddressList: string[]): void { try { this.sdkClient.pair_device(processId, spAddressList); } catch (e) { throw new Error(`[Services:pairDevice] 💥 Échec: ${e}`); } } public getAmount(): BigInt { const amount = this.sdkClient.get_available_amount(); return amount; } getDeviceAddress(): string { try { return this.sdkClient.get_address(); } catch (e) { throw new Error(`[Services:getDeviceAddress] 💥 Échec: ${e}`); } } public dumpDeviceFromMemory(): Device { try { return this.sdkClient.dump_device(); } catch (e) { throw new Error(`[Services:dumpDeviceFromMemory] 💥 Échec: ${e}`); } } public dumpNeuteredDevice(): Device | null { try { return this.sdkClient.dump_neutered_device(); } catch (e) { console.error(`[Services:dumpNeuteredDevice] 💥 Échec: ${e}`); return null; } } public getPairingProcessId(): string { try { return this.sdkClient.get_pairing_process_id(); } catch (e) { throw new Error(`[Services:getPairingProcessId] 💥 Échec (Probablement non appairé): ${e}`); } } async saveDeviceInDatabase(device: Device): Promise { const db = await Database.getInstance(); const walletStore = 'wallet'; try { console.log("[Services:saveDeviceInDatabase] 💾 Sauvegarde de l'appareil en BDD...", { pairing_process_commitment: device.pairing_process_commitment, paired_member: device.paired_member, }); const prevDevice = await this.getDeviceFromDatabase(); if (prevDevice) { // console.debug('[Services:saveDeviceInDatabase] ℹ️ Appareil précédent trouvé, suppression...'); await db.deleteObject(walletStore, '1'); } await db.addObject({ storeName: walletStore, object: { pre_id: '1', device }, key: null, }); console.log('[Services:saveDeviceInDatabase] ✅ Appareil sauvegardé avec succès'); // // Verify save // const savedDevice = await this.getDeviceFromDatabase(); // console.log('[Services:saveDeviceInDatabase] 🔎 Vérification:', { // pairing_process_commitment: savedDevice?.pairing_process_commitment, // paired_member: savedDevice?.paired_member, // }); } catch (e) { console.error('[Services:saveDeviceInDatabase] 💥 Erreur lors de la sauvegarde:', e); } } async getDeviceFromDatabase(): Promise { const db = await Database.getInstance(); const walletStore = 'wallet'; try { const dbRes = await db.getObject(walletStore, '1'); if (dbRes) { return dbRes['device']; } else { return null; } } catch (e) { throw new Error(`[Services:getDeviceFromDatabase] 💥 Échec: ${e}`); } } 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(`[Services:getMemberFromDevice] 💥 Échec: ${e}`); } } isChildRole(parent: any, child: any): boolean { try { this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child)); } catch (e) { console.error(e); return false; } return true; } rolesContainsUs(roles: Record): boolean { let us; try { us = this.sdkClient.get_pairing_process_id(); } catch (e) { // Si non appairé, nous ne pouvons être dans aucun rôle return false; } 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 { console.log("[Services:createNewDevice] ✨ Création d'un nouvel appareil..."); // We set birthday later when we have the chain tip from relay spAddress = await this.sdkClient.create_new_device(0, 'signet'); const device = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(device); console.log('[Services:createNewDevice] ✅ Appareil créé et sauvegardé.'); } catch (e) { console.error('[Services:createNewDevice] 💥 Erreur:', e); } return spAddress; } public restoreDevice(device: Device) { try { console.log("[Services:restoreDevice] 🔄 Restauration de l'appareil en mémoire..."); this.sdkClient.restore_device(device); } catch (e) { console.error(e); } } public async updateDeviceBlockHeight(): Promise { if (this.currentBlockHeight === -1) { console.warn('[Services:updateDeviceBlockHeight] ⚠️ Hauteur de bloc actuelle non définie. Skip.'); return; } let device: Device | null = null; try { device = await this.getDeviceFromDatabase(); } catch (e) { throw new Error(`[Services:updateDeviceBlockHeight] 💥 Échec de l'obtention de l'appareil depuis la BDD: ${e}`); } if (!device) { console.error('[Services:updateDeviceBlockHeight] 💥 Appareil non trouvé. Skip.'); return; } const birthday = device.sp_wallet.birthday; if (birthday === undefined || birthday === null) { console.error('[Services:updateDeviceBlockHeight] 💥 "Birthday" non trouvé. Skip.'); return; } if (birthday === 0) { console.log(`[Services:updateDeviceBlockHeight] 🎂 C'est un nouvel appareil. Définition du "birthday" à ${this.currentBlockHeight}`); // This is a new device, so current chain tip is its birthday device.sp_wallet.birthday = this.currentBlockHeight; // We also set last_scan, impossible that we need to scan earlier than this device.sp_wallet.last_scan = this.currentBlockHeight; try { // First set the updated device in memory this.sdkClient.restore_device(device); // Then save it to database await this.saveDeviceInDatabase(device); } catch (e) { throw new Error(`[Services:updateDeviceBlockHeight] 💥 Échec de la sauvegarde de l'appareil mis à jour: ${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) { console.log(`[Services:updateDeviceBlockHeight] 🏃 Rattrapage... Scan des blocs de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`); try { await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL); } catch (e) { console.error(`[Services:updateDeviceBlockHeight] 💥 Échec du scan des blocs: ${e}`); return; } // If everything went well, we can update our storage try { const device = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(device); console.log('[Services:updateDeviceBlockHeight] ✅ Scan terminé et appareil sauvegardé.'); } catch (e) { console.error(`[Services:updateDeviceBlockHeight] 💥 Échec de la sauvegarde de l'appareil après scan: ${e}`); } } else { // console.debug('[Services:updateDeviceBlockHeight] ℹ️ Portefeuille à jour. Rien à faire.'); return; } } } private async removeProcess(processId: string): Promise { const db = await Database.getInstance(); const storeName = 'processes'; try { console.log(`[Services:removeProcess] 🗑️ Suppression du processus ${processId}`); await db.deleteObject(storeName, processId); } catch (e) { console.error(e); } } public async batchSaveProcessesToDb(processes: Record) { if (Object.keys(processes).length === 0) { return; } console.log(`[Services:batchSaveProcessesToDb] 💾 Sauvegarde de ${Object.keys(processes).length} processus en BDD...`); 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) { console.error('[Services:batchSaveProcessesToDb] 💥 Échec:', 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) { console.error(`[Services:saveProcessToDb] 💥 Échec de la sauvegarde du processus ${processId}: ${e}`); } } public async saveBlobToDb(hash: string, data: Blob) { const db = await Database.getInstance(); try { await db.addObject({ storeName: 'data', object: data, key: hash, }); } catch (e) { console.error(`[Services:saveBlobToDb] 💥 Échec de la sauvegarde du blob ${hash}: ${e}`); } } 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(storages: string[], hash: string, data: Blob, ttl: number | null) { try { await storeData(storages, hash, data, ttl); } catch (e) { console.error(`[Services:saveDataToStorage] 💥 Échec du stockage du hash ${hash}: ${e}`); } } 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(`[Services:saveDiffsToDb] 💥 Échec: ${e}`); } } public async getProcess(processId: string): Promise { // 1. Essayer le cache en mémoire if (this.processesCache[processId]) { return this.processesCache[processId]; } // 2. Si non trouvé, essayer la BDD try { const db = await Database.getInstance(); const process = await db.getObject('processes', processId); if (process) { this.processesCache[processId] = process; // Mettre en cache } return process; } catch (e) { console.error(`[Services:getProcess] 💥 Échec de récupération du processus ${processId}:`, e); return null; } } public async getProcesses(): Promise> { // 1. Essayer le cache en mémoire if (Object.keys(this.processesCache).length > 0) { return this.processesCache; } // 2. Si non trouvé, charger depuis la BDD try { console.log('[Services:getProcesses] ℹ️ Cache de processus vide. Chargement depuis la BDD...'); const db = await Database.getInstance(); this.processesCache = await db.dumpStore('processes'); console.log(`[Services:getProcesses] ✅ ${Object.keys(this.processesCache).length} processus chargés en cache.`); return this.processesCache; } catch (e) { console.error('[Services:getProcesses] 💥 Échec du chargement des processus:', e); throw e; } } public async restoreProcessesFromBackUp(processes: Record) { console.log(`[Services:restoreProcessesFromBackUp] 💾 Restauration de ${Object.keys(processes).length} processus depuis un backup...`); 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) { console.log(`[Services:restoreProcessesFromDB] 🔄 Restauration de ${Object.keys(processes).length} processus depuis la BDD vers le cache...`); this.processesCache = processes; } else { console.log('[Services:restoreProcessesFromDB] ℹ️ Aucun processus à restaurer.'); } } catch (e) { throw e; } } public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { console.log('[Services:restoreSecretsFromBackUp] 💾 Restauration des secrets depuis un backup...'); 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() { console.log('[Services:restoreSecretsFromDB] 🔄 Restauration des secrets depuis la BDD vers la mémoire SDK...'); const db = await Database.getInstance(); try { 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)); console.log(`[Services:restoreSecretsFromDB] ✅ ${Object.keys(sharedSecrets).length} secrets partagés restaurés.`); } catch (e) { throw e; } } decodeValue(value: number[]): any | null { try { return this.sdkClient.decode_value(value); } catch (e) { console.error(`[Services:decodeValue] 💥 Échec: ${e}`); return null; } } async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { // Le groupe principal est "collapsed" pour ne pas polluer la console par défaut console.groupCollapsed(`[Services:decryptAttribute] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`); try { let hash = state.pcd_commitment[attribute]; if (!hash) { console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); return null; // Le 'finally' s'exécutera } let key = state.keys[attribute]; const pairingProcessId = this.getPairingProcessId(); // If key is missing, request an update and then retry if (!key) { // On crée un sous-groupe pour la logique de récupération de la clé console.group(`🔐 Gestion de la clé manquante pour '${attribute}'`); console.warn(`Vérification de l'accès et demande aux pairs...`); 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) { console.log(`⛔ Accès non autorisé. Abandon.`); console.groupEnd(); // Ferme le sous-groupe "Gestion de la clé manquante" return null; // Le 'finally' principal s'exécutera } const process = await this.getProcess(processId); if (!process) { console.error(`💥 Impossible de trouver le processus ${processId} pour ensureConnections.`); console.groupEnd(); // Ferme le sous-groupe "Gestion de la clé manquante" return null; } await this.ensureConnections(process); // We should have the key, so we're going to ask other members for it console.log(`🗣️ Demande de données aux pairs...`); await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); const maxRetries = 5; const retryDelay = 500; // delay in milliseconds let retries = 0; // On crée un sous-groupe replié pour la boucle de réessai (potentiellement verbeuse) console.groupCollapsed(`⏳ Boucle d'attente de la clé (max ${maxRetries} tentatives)`); while ((!hash || !key) && retries < maxRetries) { console.log(`(Tentative ${retries + 1}/${maxRetries})...`); await new Promise((resolve) => setTimeout(resolve, retryDelay)); // Re-read hash and key after waiting const updatedProcess = await this.getProcess(processId); const updatedState = this.getStateFromId(updatedProcess, state.state_id); if (updatedState) { hash = updatedState.pcd_commitment[attribute]; key = updatedState.keys[attribute]; } retries++; } console.groupEnd(); // Ferme le sous-groupe "Boucle d'attente" console.groupEnd(); // Ferme le sous-groupe "Gestion de la clé manquante" } // Fin de if (!key) if (hash && key) { console.log(`ℹ️ Clé et hash trouvés. Tentative de déchiffrement...`); const blob = await this.getBlobFromDb(hash); if (blob) { // 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); console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); return decoded; // Le 'finally' s'exécutera } else { throw new Error('decrypt_data returned null'); } } catch (e) { console.error(`💥 Échec du déchiffrement (decrypt_data): ${e}`); } } else { console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`); } } else { console.error(`💥 Échec: Clé ou hash manquant après ${maxRetries} tentatives pour '${attribute}'.`); } return null; // Le 'finally' s'exécutera } catch (error) { // Intercepte les erreurs inattendues non gérées console.error(`💥 Erreur inattendue dans decryptAttribute:`, error); return null; } finally { // Ce bloc est TOUJOURS exécuté, assurant que le groupe est fermé. console.groupEnd(); } } getNotifications(): any[] | null { // ... (Logique inchangée) return this.notifications; } setNotifications(notifications: any[]) { this.notifications = notifications; } async importJSON(backup: BackUp): Promise { console.log("[Services:importJSON] 📥 Importation d'un backup JSON..."); 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); console.log('[Services:importJSON] ✅ Backup importé avec succès.'); } public async createBackUp(): Promise { console.log("[Services:createBackUp] 📤 Création d'un backup..."); // Get the device from indexedDB const device = await this.getDeviceFromDatabase(); if (!device) { console.error('[Services:createBackUp] 💥 Aucun appareil chargé'); 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, }; console.log('[Services:createBackUp] ✅ Backup créé.'); return backUp; } // Device 1 wait Device 2 public device1: boolean = false; public device2Ready: boolean = false; public resetState() { this.device1 = false; this.device2Ready = false; } // --- AMÉLIORATION: Refactorisé en sous-fonctions --- public async handleHandshakeMsg(url: string, parsedMsg: any) { try { const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); // 1. Mettre à jour les infos du relais (SP Address, Block Height) this.updateRelayInfo(url, handshakeMsg); // 2. Mettre à jour la liste globale des membres this.updateGlobalMembersList(handshakeMsg.peers_list); // 3. Lancer la synchronisation (lourde) des processus en arrière-plan // (Anciennement dans un setTimeout, maintenant dans une fonction dédiée) this.syncProcessesFromHandshake(handshakeMsg.processes_list, url).catch((e) => console.error(`[Services:syncProcessesFromHandshake] 💥 Échec de la synchro des processus pour ${url}:`, e)); } catch (e) { console.error(`[Services:handleHandshakeMsg] 💥 Échec de l'analyse du message init:`, e); } } /** * Sous-fonction de handleHandshakeMsg: Met à jour les infos du relais. */ private updateRelayInfo(url: string, handshakeMsg: HandshakeMessage) { if (handshakeMsg.sp_address) { this.updateRelay(url, handshakeMsg.sp_address); this.relayAddresses[url] = handshakeMsg.sp_address; this.resolveRelayReady(); } console.log(`[Services:updateRelayInfo] ℹ️ Handshake reçu de ${url}:`, handshakeMsg); this.currentBlockHeight = handshakeMsg.chain_tip; console.log(`[Services:updateRelayInfo] ⛓️ Hauteur de bloc actuelle: ${this.currentBlockHeight}`); this.updateDeviceBlockHeight(); } /** * Sous-fonction de handleHandshakeMsg: Met à jour la liste globale des membres. */ private updateGlobalMembersList(peers_list: Record | null) { console.log('[Services:updateGlobalMembersList] 🔄 Mise à jour de la liste des membres...'); if (!peers_list || Object.keys(peers_list).length === 0) { console.log('[Services:updateGlobalMembersList] ℹ️ Aucune liste de pairs reçue.'); return; } if (this.membersList && Object.keys(this.membersList).length === 0) { console.log('[Services:updateGlobalMembersList] ⚠️ Écrasement de la membersList vide avec la liste des pairs reçue.'); this.membersList = peers_list; } else { console.log('[Services:updateGlobalMembersList] ➕ Incrémentation de la membersList existante avec les nouveaux pairs.'); for (const [processId, member] of Object.entries(peers_list)) { this.membersList[processId] = member as Member; } } console.log(`[Services:updateGlobalMembersList] ✅ Liste des membres mise à jour. Total: ${Object.keys(this.membersList).length}`); } /** * Sous-fonction de handleHandshakeMsg: Gère la logique complexe de synchro des processus. */ private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap, url: string) { if (!newProcesses || Object.keys(newProcesses).length === 0) { console.debug(`[Services:syncProcesses] ℹ️ Reçu une liste de processus vide de ${url}.`); return; } console.log(`[Services:syncProcesses] 🔄 Synchronisation de ${Object.keys(newProcesses).length} processus depuis ${url}...`); if (this.processesCache && Object.keys(this.processesCache).length === 0) { // Cas simple: BDD vide, on sauvegarde tout console.log('[Services:syncProcesses] ℹ️ Cache vide, sauvegarde en batch...'); try { await this.batchSaveProcessesToDb(newProcesses); } catch (e) { console.error('[Services:syncProcesses] 💥 Échec de la sauvegarde en batch:', e); } } else { // Cas complexe: Mise à jour différentielle const toSave: Record = {}; for (const [processId, process] of Object.entries(newProcesses)) { const existing = await this.getProcess(processId); if (existing) { // --- Logique de mise à jour d'un processus existant --- let newStates: string[] = []; let newRoles: Record[] = []; for (const state of process.states) { if (!state || !state.state_id) continue; if (state.state_id === EMPTY32BYTES) { // ... (Logique du 'tip') ... const existingTip = existing.states[existing.states.length - 1].commited_in; if (existingTip !== state.commited_in) { console.log(`[Services:syncProcesses] ℹ️ Nouveau 'tip' trouvé pour ${processId}`); existing.states.pop(); existing.states.push(state); toSave[processId] = existing; } } else if (!this.lookForStateId(existing, state.state_id)) { // ... (Logique d'ajout de nouvel état) ... console.log(`[Services:syncProcesses] ℹ️ Nouvel état ${state.state_id} trouvé pour ${processId}`); const existingLastState = existing.states.pop(); if (!existingLastState) { console.error(`[Services:syncProcesses] 💥 Échec: impossible de trouver le dernier état pour ${processId}`); break; } existing.states.push(state); existing.states.push(existingLastState); toSave[processId] = existing; if (this.rolesContainsUs(state.roles)) { newStates.push(state.state_id); newRoles.push(state.roles); } } else { // ... (Logique de vérification des clés pour un état existant) ... const existingState = this.getStateFromId(existing, state.state_id); if (existingState!.keys && Object.keys(existingState!.keys).length != 0) { continue; // On a déjà les clés } else { if (this.rolesContainsUs(state.roles)) { console.log(`[Services:syncProcesses] ℹ️ État ${state.state_id} (processus ${processId}) existe, mais clés manquantes. Demande...`); newStates.push(state.state_id); newRoles.push(state.roles); } else { continue; // Pas notre état } } } } if (newStates.length != 0) { console.log(`[Services:syncProcesses] 📞 Demande de données pour ${newStates.length} nouvel(s) état(s) dans ${processId}`); await this.ensureConnections(existing); await this.requestDataFromPeers(processId, newStates, newRoles); } } else { // Processus totalement nouveau, on l'ajoute console.log(`[Services:syncProcesses] ℹ️ Nouveau processus ${processId} trouvé. Ajout.`); toSave[processId] = process; } } if (toSave && Object.keys(toSave).length > 0) { console.log('[Services:syncProcesses] 💾 Sauvegarde en batch des processus mis à jour/nouveaux...', Object.keys(toSave)); await this.batchSaveProcessesToDb(toSave); } } console.log('[Services:syncProcesses] ✅ Synchronisation terminée. Émission de l\'événement "processes-updated".'); document.dispatchEvent(new CustomEvent('processes-updated')); } 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. */ private async waitForHandshakeMessage(timeoutMs: number = 10000): 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.values(this.relayAddresses).some((addr) => addr !== '')) { console.log('[Services:waitForHandshakeMessage] ✅ Handshake reçu (membres ou relais présents)'); resolve(); return; } // Check timeout if (Date.now() - startTime >= timeoutMs) { reject(new Error(`[Services:waitForHandshakeMessage] ❌ Aucun handshake reçu après ${timeoutMs}ms`)); 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 { // console.debug('[Services:getAddressesForMemberId] 🔍 Recherche d\'adresses pour:', memberId); if (Object.keys(this.membersList).length === 0) { console.error('[Services:getAddressesForMemberId] ❌ ERREUR: membersList est VIDE. Impossible de trouver le membre:', memberId); return null; } if (!this.membersList[memberId]) { console.error(`[Services:getAddressesForMemberId] ❌ ERREUR: Membre non trouvé dans membersList: ${memberId}`); // console.debug('Membres actuels:', this.membersList); return null; } return this.membersList[memberId].sp_addresses; } catch (e) { console.error('[Services:getAddressesForMemberId] 💥 Erreur inattendue:', 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']; console.warn(`[Services:handleCommitError] ⚠️ Erreur de Commit reçue: ${errorMsg}`); const dontRetry = ['State is identical to the previous state', 'Not enough valid proofs', 'Not enough members to validate']; if (dontRetry.includes(errorMsg)) { console.warn('[Services:handleCommitError] ⏩ Erreur non récupérable. Pas de nouvelle tentative.'); return; } // Wait and retry console.warn('[Services:handleCommitError] 🔄 Tentative de renvoi du Commit dans 1s...'); 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) { // Cas spécial pour les processus venant d'être créés ? 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; // Pas appairé } 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)) { if (newMyProcesses.has(processId)) { continue; } try { const roles = this.getRoles(process); if (roles && this.rolesContainsUs(roles)) { newMyProcesses.add(processId); } } catch (e) { console.error(e); } } this.myProcesses = newMyProcesses; // atomic update return Array.from(this.myProcesses); } catch (e) { console.error('[Services:getMyProcesses] 💥 Échec:', e); return null; } } public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record[]) { console.log(`[Services:requestDataFromPeers] 🗣️ Demande de données pour ${processId} (états: ${stateIds.join(', ')})`); const membersList = this.getAllMembers(); try { const res = this.sdkClient.request_data(processId, stateIds, roles, membersList); await this.handleApiReturn(res); } catch (e) { console.error(e); } } public hexToBlob(hexString: string): Blob { const uint8Array = this.hexToUInt8Array(hexString); return new Blob([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(`[Services:validateMerkleProof] 💥 Échec: ${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); } }