diff --git a/src/services/service.ts b/src/services/service.ts index 18a295f..3dfeb9d 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1,2522 +1,2500 @@ -// @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 pendingKeyRequests: Map void> = new Map(); - // 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 { - console.info(`[ConnectionCheck] 🔄 Démarrage de la vérification des connexions pour le processus (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. (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 }; - } - - /** - * Met à jour un processus. - * Gère la logique complexe de recherche du dernier état "commited" - * et tente une auto-approbation si nécessaire. - */ - public async updateProcess( - processId: string, // Changement : on reçoit l'ID, pas l'objet Process - newData: Record, // Changement : on reçoit les nouvelles données - privateFields: string[], // Changement : on reçoit les champs privés - roles: Record | null, - ): Promise { - // 1. Récupérer le processus - const process = await this.getProcess(processId); - if (!process) { - throw new Error(`[Services:updateProcess] Processus ${processId} non trouvé`); - } - - // --- DEBUT DE LA LOGIQUE DÉPLACÉE DU ROUTEUR --- - - // 2. Trouver le dernier état et le "réparer" si nécessaire - let lastState = this.getLastCommitedState(process); - let currentProcess = process; // Garde une trace du processus à jour - - if (!lastState) { - console.warn(`[Services:updateProcess] ⚠️ Processus ${processId} n'a pas d'état "commited". Tentative d'auto-approbation...`); - const firstState = process.states[0]; - - if (this.rolesContainsUs(firstState.roles)) { - // ON REMPLACE LE setTimeout PAR UN VRAI AWAIT - console.log(`[Services:updateProcess] Auto-approbation de l'état ${firstState.state_id}...`); - const approveChangeRes = await this.approveChange(processId, firstState.state_id); - await this.handleApiReturn(approveChangeRes); // On attend que la BDD soit à jour - - console.log(`[Services:updateProcess] Création de la PRD update pour l'état ${firstState.state_id}...`); - const prdUpdateRes = await this.createPrdUpdate(processId, firstState.state_id); - await this.handleApiReturn(prdUpdateRes); // On attend à nouveau - } else { - if (firstState.validation_tokens.length > 0) { - console.log(`[Services:updateProcess] Création de la PRD update (sans approbation) pour l'état ${firstState.state_id}...`); - const res = await this.createPrdUpdate(processId, firstState.state_id); - await this.handleApiReturn(res); - } - } - - // 3. Re-synchroniser l'état après nos actions - console.log(`[Services:updateProcess] Re-vérification de l'état "commited" après auto-approbation...`); - const updatedProcess = await this.getProcess(processId); // On recharge depuis la BDD - if (!updatedProcess) throw new Error('Le processus a disparu après la tentative de réparation'); - - currentProcess = updatedProcess; // On met à jour notre référence - lastState = this.getLastCommitedState(currentProcess); - - if (!lastState) { - // Si ça échoue toujours, on abandonne - throw new Error("Le processus n'a toujours pas d'état 'commited' après la tentative de réparation."); - } - console.log(`[Services:updateProcess] ✅ État "commited" ${lastState.state_id} trouvé.`); - } - - const lastStateIndex = this.getLastCommitedStateIndex(currentProcess); - if (lastStateIndex === null) { - // Sécurité, bien que logiquement couvert par le bloc ci-dessus - throw new Error("Impossible de trouver l'index du dernier état 'commited'."); - } - - // 4. Calculer les diffs (logique de séparation privée/publique) - const privateData: Record = {}; - const publicData: Record = {}; - - for (const field of Object.keys(newData)) { - if (lastState.public_data[field]) { - publicData[field] = newData[field]; - continue; - } - if (privateFields.includes(field)) { - privateData[field] = newData[field]; - continue; - } - // Logique de recherche dans l'historique - for (let i = lastStateIndex; i >= 0; i--) { - const state = currentProcess.states[i]; - if (state.pcd_commitment[field]) { - privateData[field] = newData[field]; - break; - } - } - if (privateData[field]) continue; - - // Par défaut, c'est public - publicData[field] = newData[field]; - } - - // --- FIN DE LA LOGIQUE DÉPLACÉE DU ROUTEUR --- - - // 5. Exécuter la mise à jour - if (!roles) { - roles = this.getRoles(currentProcess); // Important : utiliser currentProcess - } - - console.log('[Services:updateProcess] ℹ️ Préparation des données binaires et JSON...'); - 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 { - console.log('[Services:updateProcess] 🚀 Appel de sdkClient.update_process...'); - const result = this.sdkClient.update_process( - currentProcess, // Utiliser le processus potentiellement mis à jour - encodedPrivateData, - roles, - encodedPublicData, - this.getAllMembers(), - ); - - if (result.updated_process) { - console.log(`[Services:updateProcess] ✅ Processus mis à jour. Vérification des connexions...`); - // On s'assure qu'on est connecté aux membres du nouvel état - 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.states[0].commited_in; - 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); - } - } - - this._resolvePendingKeyRequests(updatedProcess.current_process); - - // Vérifier la logique métier spécifique au pairing - await this.checkAndConfirmPairing(processId, updatedProcess); - } - - private _resolvePendingKeyRequests(process: Process) { - if (this.pendingKeyRequests.size === 0) { - return; // Optimisation : ne rien faire si personne n'attend - } - - console.log(`[Services:KeyResolver] 🔍 Vérification de ${this.pendingKeyRequests.size} requête(s) de clé en attente...`); - - for (const state of process.states) { - if (!state.keys || Object.keys(state.keys).length === 0) { - continue; // Pas de clés dans cet état - } - - for (const [attributeName, key] of Object.entries(state.keys)) { - const requestId = `${process.process_id}_${state.state_id}_${attributeName}`; - - // Avons-nous une requête en attente pour CETTE clé ? - if (this.pendingKeyRequests.has(requestId)) { - console.log(`[Services:KeyResolver] ✅ Résolution de la requête pour ${requestId}`); - - // Récupérer la fonction "resolve" et l'appeler avec la clé - const resolveCallback = this.pendingKeyRequests.get(requestId); - if (resolveCallback) { - resolveCallback(key as string); - } - - // Nettoyer la requête - this.pendingKeyRequests.delete(requestId); - } - } - } - } - - 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; - } - } - - // La fonction principale est maintenant beaucoup plus simple à lire. - // Elle sert d'orchestrateur. - async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { - console.groupCollapsed(`[Services:decryptAttribute] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`); - - try { - let hash = state.pcd_commitment[attribute]; - let key = state.keys[attribute]; - const pairingProcessId = this.getPairingProcessId(); - - // 1. Garde : A-t-on au moins le hash ? - if (!hash) { - console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); - return null; - } - - // 2. Si la clé est manquante, on la récupère - if (!key) { - // 2a. Vérifier l'accès (logique extraite) - if (!this._checkAccess(state, attribute, pairingProcessId)) { - console.log(`⛔ Accès non autorisé. Abandon.`); - return null; - } - - // 2b. Tenter de récupérer la clé (logique extraite) - const result = await this._fetchMissingKey(processId, state, attribute); - hash = result.hash; // Mettre à jour le hash (il a pu être rafraîchi) - key = result.key; // Mettre à jour la clé - } - - // 3. Si on a tout (soit depuis le début, soit après récupération) - if (hash && key) { - console.log(`ℹ️ Clé et hash trouvés. Tentative de déchiffrement...`); - const blob = await this.getBlobFromDb(hash); - - if (!blob) { - console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`); - return null; - } - - try { - const buf = await blob.arrayBuffer(); - const cipher = new Uint8Array(buf); - const keyUIntArray = this.hexToUInt8Array(key); - - const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher); - if (!clear) { - throw new Error('decrypt_data returned null'); - } - - const decoded = this.sdkClient.decode_value(clear); - console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); - return decoded; - } catch (e) { - console.error(`💥 Échec du déchiffrement (decrypt_data): ${e}`); - return null; - } - } - - // 4. Échec final si la clé ou le hash manque toujours - console.error(`💥 Échec: Clé ou hash manquant après tentatives pour '${attribute}'.`); - return null; - } catch (error) { - console.error(`💥 Erreur inattendue dans decryptAttribute:`, error); - return null; - } finally { - // Garantit que le groupe principal est TOUJOURS fermé - console.groupEnd(); - } - } - - /** - * NOUVELLE MÉTHODE PRIVÉE - * Vérifie si l'utilisateur courant a le droit d'accéder à cet attribut - * en se basant sur les rôles du state. - */ - private _checkAccess(state: ProcessState, attribute: string, pairingProcessId: string): boolean { - const roles = state.roles; - - // Utilise .some() pour une lecture plus claire et un arrêt anticipé - // "Existe-t-il AU MOINS UN rôle..." - return Object.values(roles).some((role) => { - // "...tel que l'utilisateur est membre..." - const isMember = role.members.includes(pairingProcessId); - if (!isMember) { - return false; // Passe au rôle suivant - } - - // "...ET ce rôle a AU MOINS UNE règle de validation qui inclut cet attribut" - return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute)); - }); - } - - /** - * NOUVELLE MÉTHODE PRIVÉE (MODIFIÉE) - * Gère la logique de demande aux pairs en utilisant un système de Promise au lieu de polling. - */ - private async _fetchMissingKey(processId: string, state: ProcessState, attribute: string): Promise<{ hash: string | null; key: string | null }> { - console.group(`🔐 Gestion de la clé manquante pour '${attribute}' (via Promise)`); - - try { - const process = await this.getProcess(processId); - if (!process) { - console.error(`💥 Impossible de trouver le processus ${processId} pour ensureConnections.`); - return { hash: null, key: null }; - } - - // 1. Demander les connexions et envoyer la requête (comme avant) - await this.ensureConnections(process); - console.log(`🗣️ Demande de données aux pairs...`); - await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); - - // 2. CRÉER LA PROMISE D'ATTENTE - const requestId = `${processId}_${state.state_id}_${attribute}`; - const keyRequestPromise = new Promise((resolve, reject) => { - // Sécurité : Définir un timeout (ex: 15 secondes) - const timeout = setTimeout(() => { - console.warn(`⌛ Timeout: La clé pour '${attribute}' n'est jamais arrivée.`); - this.pendingKeyRequests.delete(requestId); // Nettoyer - reject(new Error(`Timeout waiting for key: ${attribute}`)); - }, 15000); // 15 secondes - - // Enregistrer le "resolve" pour qu'il soit appelé de l'extérieur - this.pendingKeyRequests.set(requestId, (key: string) => { - clearTimeout(timeout); // Annuler le timeout - console.log(`✅ Clé reçue via listener pour '${attribute}'!`); - resolve(key); - }); - }); - - // 3. Attendre la clé - const receivedKey = await keyRequestPromise; - - // 4. Re-vérifier l'état (le hash a pu changer) - const updatedProcess = await this.getProcess(processId); - const updatedState = this.getStateFromId(updatedProcess, state.state_id); - const updatedHash = updatedState ? updatedState.pcd_commitment[attribute] : state.pcd_commitment[attribute]; - - return { hash: updatedHash, key: receivedKey }; - } catch (e) { - console.error(`💥 Erreur durant _fetchMissingKey: ${e}`); - return { hash: null, key: null }; - } finally { - 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); - } -} +// @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 pendingKeyRequests: Map void> = new Map(); + // 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 { + console.info(`[ConnectionCheck] 🔄 Démarrage de la vérification des connexions pour le processus (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. (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; + + 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); + } + } + } + + 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 }; + } + + /** + * Met à jour un processus. + * Gère la logique complexe de recherche du dernier état "commited" + * et tente une auto-approbation si nécessaire. + */ + public async updateProcess( + processId: string, // Changement : on reçoit l'ID, pas l'objet Process + newData: Record, // Changement : on reçoit les nouvelles données + privateFields: string[], // Changement : on reçoit les champs privés + roles: Record | null, + ): Promise { + // 1. Récupérer le processus + const process = await this.getProcess(processId); + if (!process) { + throw new Error(`[Services:updateProcess] Processus ${processId} non trouvé`); + } + + // --- DEBUT DE LA LOGIQUE DÉPLACÉE DU ROUTEUR --- + + // 2. Trouver le dernier état et le "réparer" si nécessaire + let lastState = this.getLastCommitedState(process); + let currentProcess = process; // Garde une trace du processus à jour + + if (!lastState) { + console.warn(`[Services:updateProcess] ⚠️ Processus ${processId} n'a pas d'état "commited". Tentative d'auto-approbation...`); + const firstState = process.states[0]; + + if (this.rolesContainsUs(firstState.roles)) { + // ON REMPLACE LE setTimeout PAR UN VRAI AWAIT + console.log(`[Services:updateProcess] Auto-approbation de l'état ${firstState.state_id}...`); + const approveChangeRes = await this.approveChange(processId, firstState.state_id); + await this.handleApiReturn(approveChangeRes); // On attend que la BDD soit à jour + + console.log(`[Services:updateProcess] Création de la PRD update pour l'état ${firstState.state_id}...`); + const prdUpdateRes = await this.createPrdUpdate(processId, firstState.state_id); + await this.handleApiReturn(prdUpdateRes); // On attend à nouveau + } else { + if (firstState.validation_tokens.length > 0) { + console.log(`[Services:updateProcess] Création de la PRD update (sans approbation) pour l'état ${firstState.state_id}...`); + const res = await this.createPrdUpdate(processId, firstState.state_id); + await this.handleApiReturn(res); + } + } + + // 3. Re-synchroniser l'état après nos actions + console.log(`[Services:updateProcess] Re-vérification de l'état "commited" après auto-approbation...`); + const updatedProcess = await this.getProcess(processId); // On recharge depuis la BDD + if (!updatedProcess) throw new Error('Le processus a disparu après la tentative de réparation'); + + currentProcess = updatedProcess; // On met à jour notre référence + lastState = this.getLastCommitedState(currentProcess); + + if (!lastState) { + // Si ça échoue toujours, on abandonne + throw new Error("Le processus n'a toujours pas d'état 'commited' après la tentative de réparation."); + } + console.log(`[Services:updateProcess] ✅ État "commited" ${lastState.state_id} trouvé.`); + } + + const lastStateIndex = this.getLastCommitedStateIndex(currentProcess); + if (lastStateIndex === null) { + // Sécurité, bien que logiquement couvert par le bloc ci-dessus + throw new Error("Impossible de trouver l'index du dernier état 'commited'."); + } + + // 4. Calculer les diffs (logique de séparation privée/publique) + const privateData: Record = {}; + const publicData: Record = {}; + + for (const field of Object.keys(newData)) { + if (lastState.public_data[field]) { + publicData[field] = newData[field]; + continue; + } + if (privateFields.includes(field)) { + privateData[field] = newData[field]; + continue; + } + // Logique de recherche dans l'historique + for (let i = lastStateIndex; i >= 0; i--) { + const state = currentProcess.states[i]; + if (state.pcd_commitment[field]) { + privateData[field] = newData[field]; + break; + } + } + if (privateData[field]) continue; + + // Par défaut, c'est public + publicData[field] = newData[field]; + } + + // --- FIN DE LA LOGIQUE DÉPLACÉE DU ROUTEUR --- + + // 5. Exécuter la mise à jour + if (!roles) { + roles = this.getRoles(currentProcess); // Important : utiliser currentProcess + } + + console.log('[Services:updateProcess] ℹ️ Préparation des données binaires et JSON...'); + 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 { + console.log('[Services:updateProcess] 🚀 Appel de sdkClient.update_process...'); + const result = this.sdkClient.update_process( + currentProcess, // Utiliser le processus potentiellement mis à jour + encodedPrivateData, + roles, + encodedPublicData, + this.getAllMembers(), + ); + + if (result.updated_process) { + console.log(`[Services:updateProcess] ✅ Processus mis à jour. Vérification des connexions...`); + // On s'assure qu'on est connecté aux membres du nouvel état + 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); + + } catch (e) { + console.error(`[Services:parseCipher] 💥 Échec critique du déchiffrement: ${e}`); + } + } + + 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.states[0].commited_in; + 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); + } + } + + this._resolvePendingKeyRequests(updatedProcess.current_process); + + // Vérifier la logique métier spécifique au pairing + await this.checkAndConfirmPairing(processId, updatedProcess); + } + + private _resolvePendingKeyRequests(process: Process) { + if (this.pendingKeyRequests.size === 0) { + return; // Optimisation : ne rien faire si personne n'attend + } + + console.log(`[Services:KeyResolver] 🔍 Vérification de ${this.pendingKeyRequests.size} requête(s) de clé en attente...`); + + for (const state of process.states) { + if (!state.keys || Object.keys(state.keys).length === 0) { + continue; // Pas de clés dans cet état + } + + for (const [attributeName, key] of Object.entries(state.keys)) { + const requestId = `${process.process_id}_${state.state_id}_${attributeName}`; + + // Avons-nous une requête en attente pour CETTE clé ? + if (this.pendingKeyRequests.has(requestId)) { + console.log(`[Services:KeyResolver] ✅ Résolution de la requête pour ${requestId}`); + + // Récupérer la fonction "resolve" et l'appeler avec la clé + const resolveCallback = this.pendingKeyRequests.get(requestId); + if (resolveCallback) { + resolveCallback(key as string); + } + + // Nettoyer la requête + this.pendingKeyRequests.delete(requestId); + } + } + } + } + + 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; + } + } + + // La fonction principale est maintenant beaucoup plus simple à lire. + // Elle sert d'orchestrateur. + async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { + console.groupCollapsed(`[Services:decryptAttribute] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`); + + try { + let hash = state.pcd_commitment[attribute]; + let key = state.keys[attribute]; + const pairingProcessId = this.getPairingProcessId(); + + // 1. Garde : A-t-on au moins le hash ? + if (!hash) { + console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); + return null; + } + + // 2. Si la clé est manquante, on la récupère + if (!key) { + // 2a. Vérifier l'accès (logique extraite) + if (!this._checkAccess(state, attribute, pairingProcessId)) { + console.log(`⛔ Accès non autorisé. Abandon.`); + return null; + } + + // 2b. Tenter de récupérer la clé (logique extraite) + const result = await this._fetchMissingKey(processId, state, attribute); + hash = result.hash; // Mettre à jour le hash (il a pu être rafraîchi) + key = result.key; // Mettre à jour la clé + } + + // 3. Si on a tout (soit depuis le début, soit après récupération) + if (hash && key) { + console.log(`ℹ️ Clé et hash trouvés. Tentative de déchiffrement...`); + const blob = await this.getBlobFromDb(hash); + + if (!blob) { + console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`); + return null; + } + + try { + const buf = await blob.arrayBuffer(); + const cipher = new Uint8Array(buf); + const keyUIntArray = this.hexToUInt8Array(key); + + const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher); + if (!clear) { + throw new Error('decrypt_data returned null'); + } + + const decoded = this.sdkClient.decode_value(clear); + console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); + return decoded; + } catch (e) { + console.error(`💥 Échec du déchiffrement (decrypt_data): ${e}`); + return null; + } + } + + // 4. Échec final si la clé ou le hash manque toujours + console.error(`💥 Échec: Clé ou hash manquant après tentatives pour '${attribute}'.`); + return null; + } catch (error) { + console.error(`💥 Erreur inattendue dans decryptAttribute:`, error); + return null; + } finally { + // Garantit que le groupe principal est TOUJOURS fermé + console.groupEnd(); + } + } + + /** + * NOUVELLE MÉTHODE PRIVÉE + * Vérifie si l'utilisateur courant a le droit d'accéder à cet attribut + * en se basant sur les rôles du state. + */ + private _checkAccess(state: ProcessState, attribute: string, pairingProcessId: string): boolean { + const roles = state.roles; + + // Utilise .some() pour une lecture plus claire et un arrêt anticipé + // "Existe-t-il AU MOINS UN rôle..." + return Object.values(roles).some((role) => { + // "...tel que l'utilisateur est membre..." + const isMember = role.members.includes(pairingProcessId); + if (!isMember) { + return false; // Passe au rôle suivant + } + + // "...ET ce rôle a AU MOINS UNE règle de validation qui inclut cet attribut" + return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute)); + }); + } + + /** + * NOUVELLE MÉTHODE PRIVÉE (MODIFIÉE) + * Gère la logique de demande aux pairs en utilisant un système de Promise au lieu de polling. + */ + private async _fetchMissingKey(processId: string, state: ProcessState, attribute: string): Promise<{ hash: string | null; key: string | null }> { + console.group(`🔐 Gestion de la clé manquante pour '${attribute}' (via Promise)`); + + try { + const process = await this.getProcess(processId); + if (!process) { + console.error(`💥 Impossible de trouver le processus ${processId} pour ensureConnections.`); + return { hash: null, key: null }; + } + + // 1. Demander les connexions et envoyer la requête (comme avant) + await this.ensureConnections(process); + console.log(`🗣️ Demande de données aux pairs...`); + await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); + + // 2. CRÉER LA PROMISE D'ATTENTE + const requestId = `${processId}_${state.state_id}_${attribute}`; + const keyRequestPromise = new Promise((resolve, reject) => { + // Sécurité : Définir un timeout (ex: 15 secondes) + const timeout = setTimeout(() => { + console.warn(`⌛ Timeout: La clé pour '${attribute}' n'est jamais arrivée.`); + this.pendingKeyRequests.delete(requestId); // Nettoyer + reject(new Error(`Timeout waiting for key: ${attribute}`)); + }, 15000); // 15 secondes + + // Enregistrer le "resolve" pour qu'il soit appelé de l'extérieur + this.pendingKeyRequests.set(requestId, (key: string) => { + clearTimeout(timeout); // Annuler le timeout + console.log(`✅ Clé reçue via listener pour '${attribute}'!`); + resolve(key); + }); + }); + + // 3. Attendre la clé + const receivedKey = await keyRequestPromise; + + // 4. Re-vérifier l'état (le hash a pu changer) + const updatedProcess = await this.getProcess(processId); + const updatedState = this.getStateFromId(updatedProcess, state.state_id); + const updatedHash = updatedState ? updatedState.pcd_commitment[attribute] : state.pcd_commitment[attribute]; + + return { hash: updatedHash, key: receivedKey }; + } catch (e) { + console.error(`💥 Erreur durant _fetchMissingKey: ${e}`); + return { hash: null, key: null }; + } finally { + 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); + } +} +