diff --git a/WORKFLOW_PAIRING_ANALYSIS.md b/WORKFLOW_PAIRING_ANALYSIS.md new file mode 100644 index 0000000..c2bad62 --- /dev/null +++ b/WORKFLOW_PAIRING_ANALYSIS.md @@ -0,0 +1,385 @@ +# Analyse du Workflow de Pairing - Processus de Couplage d'Appareils + +## Introduction + +Ce document présente une analyse complète du workflow de pairing (processus de couplage) dans l'écosystème 4NK, qui permet à deux appareils de s'associer de manière sécurisée. Le processus implique à la fois le code TypeScript côté client (`ihm_client_dev2`) et les fonctionnalités WebAssembly compilées depuis Rust (`sdk_client`). + +## Vue d'ensemble du Processus + +Le workflow de pairing est déclenché par la fonction `onCreateButtonClick()` et suit une séquence d'actions coordonnées entre l'interface utilisateur, les services JavaScript et le module WebAssembly. + +## Étapes Détaillées du Workflow + +### 1. Déclenchement Initial + +**Fonction :** `onCreateButtonClick()` +**Localisation :** `src/utils/sp-address.utils.ts:152-160` + +```@/home/ank/dev/ihm_client_dev2/src/utils/sp-address.utils.ts#152:160 +async function onCreateButtonClick() { + try { + await prepareAndSendPairingTx(); + // Don't call confirmPairing immediately - it will be called when the pairing process is complete + console.log('Pairing process initiated. Waiting for completion...'); + } catch (e) { + console.error(`onCreateButtonClick error: ${e}`); + } +} +``` + +**Actions :** +- Appel de `prepareAndSendPairingTx()` +- Gestion d'erreur avec logging +- Attente de la complétion du processus + +### 2. Préparation et Envoi de la Transaction de Pairing + +**Fonction :** `prepareAndSendPairingTx()` +**Localisation :** `src/utils/sp-address.utils.ts:162-201` + +**Actions principales :** + +#### 2.1 Initialisation du Service +```typescript +const service = await Services.getInstance(); +const relayAddress = service.getAllRelays(); +``` + +#### 2.2 Création du Processus de Pairing +```typescript +const createPairingProcessReturn = await service.createPairingProcess("", []); +``` + +#### 2.3 Vérification des Connexions +```typescript +await service.checkConnections( + createPairingProcessReturn.updated_process.current_process, + createPairingProcessReturn.updated_process.current_process.states[0].state_id +); +``` + +#### 2.4 Configuration des Identifiants +```typescript +service.setProcessId(createPairingProcessReturn.updated_process.process_id); +service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id); +``` + +#### 2.5 Mise à Jour du Device +```typescript +const currentDevice = await service.getDeviceFromDatabase(); +if (currentDevice) { + currentDevice.pairing_process_commitment = createPairingProcessReturn.updated_process.process_id; + await service.saveDeviceInDatabase(currentDevice); +} +``` + +#### 2.6 Traitement du Retour API +```typescript +await service.handleApiReturn(createPairingProcessReturn); +``` + +### 3. Création du Processus de Pairing (Côté Service) + +**Fonction :** `createPairingProcess()` +**Localisation :** `src/services/service.ts:334-371` + +**Paramètres :** +- `userName`: Nom d'utilisateur (vide dans ce cas) +- `pairWith`: Liste des adresses à coupler (vide initialement) + +**Actions :** + +#### 3.1 Vérification de l'État de Pairing +```typescript +if (this.sdkClient.is_paired()) { + throw new Error('Device already paired'); +} +``` + +#### 3.2 Préparation des Données +```typescript +const myAddress: string = this.sdkClient.get_address(); +pairWith.push(myAddress); + +const privateData = { + description: 'pairing', + counter: 0, +}; + +const publicData = { + memberPublicName: userName, + pairedAddresses: pairWith, +}; +``` + +#### 3.3 Définition des Rôles +```typescript +const roles: Record = { + pairing: { + members: [], + validation_rules: [{ + quorum: 1.0, + fields: validation_fields, + min_sig_member: 1.0, + }], + storages: [STORAGEURL] + }, +}; +``` + +#### 3.4 Appel de Création du Processus +```typescript +return this.createProcess(privateData, publicData, roles); +``` + +### 4. Création du Processus (Côté WebAssembly) + +**Fonction :** `create_new_process()` +**Localisation :** `sdk_client/src/api.rs:1218-1264` + +**Actions principales :** + +#### 4.1 Validation des Rôles +```rust +if roles.is_empty() { + return Err(ApiError { message: "Roles can't be empty".to_owned() }); +} +``` + +#### 4.2 Création de la Transaction +```rust +let relay_address: SilentPaymentAddress = relay_address.try_into()?; +let tx = create_transaction_for_addresses(&local_device, &freezed_utxos, &vec![relay_address], fee_rate_checked)?; +let unsigned_transaction = SpClient::finalize_transaction(tx)?; +``` + +#### 4.3 Gestion des Secrets Partagés +```rust +let new_secrets = get_shared_secrets_in_transaction(&unsigned_transaction, &vec![relay_address])?; +let mut shared_secrets = lock_shared_secrets()?; +for (address, secret) in new_secrets { + shared_secrets.confirm_secret_for_address(secret, address); +} +``` + +#### 4.4 Création de l'État du Processus +```rust +let process_id = OutPoint::new(unsigned_transaction.unsigned_tx.as_ref().unwrap().txid(), 0); +let mut new_state = ProcessState::new(process_id, private_data.clone(), public_data.clone(), roles.clone())?; +let mut process = Process::new(process_id); +``` + +### 5. Vérification des Connexions + +**Fonction :** `checkConnections()` +**Localisation :** `src/services/service.ts:232-289` + +**Actions :** + +#### 5.1 Validation des États +```typescript +if (process.states.length < 2) { + throw new Error('Process doesn\'t have any state yet'); +} +``` + +#### 5.2 Extraction des Rôles et Membres +```typescript +let roles: Record | null = null; +if (!stateId) { + roles = process.states[process.states.length - 2].roles; +} else { + roles = process.states.find(state => state.state_id === stateId)?.roles || null; +} +``` + +#### 5.3 Gestion des Processus de Pairing +```typescript +if (members.size === 0) { + // This must be a pairing process + const publicData = process.states[0]?.public_data; + if (!publicData || !publicData['pairedAddresses']) { + throw new Error('Not a pairing process'); + } + const decodedAddresses = this.decodeValue(publicData['pairedAddresses']); + members.add({ sp_addresses: decodedAddresses }); +} +``` + +#### 5.4 Connexion aux Adresses Non Connectées +```typescript +if (unconnectedAddresses && unconnectedAddresses.size != 0) { + const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses)); + await this.handleApiReturn(apiResult); +} +``` + +### 6. Traitement du Retour API + +**Fonction :** `handleApiReturn()` +**Localisation :** `src/services/service.ts:648-775` + +**Actions principales :** + +#### 6.1 Signature de Transaction +```typescript +if (apiReturn.partial_tx) { + const res = this.sdkClient.sign_transaction(apiReturn.partial_tx); + apiReturn.new_tx_to_send = res.new_tx_to_send; +} +``` + +#### 6.2 Envoi de Transaction +```typescript +if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { + this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); + await new Promise(r => setTimeout(r, 500)); +} +``` + +#### 6.3 Gestion des Secrets +```typescript +if (apiReturn.secrets) { + const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets; + const confirmedSecrets = apiReturn.secrets.shared_secrets; + // Sauvegarde en base de données +} +``` + +#### 6.4 Mise à Jour du Processus +```typescript +if (apiReturn.updated_process) { + const updatedProcess = apiReturn.updated_process; + const processId: string = updatedProcess.process_id; + await this.saveProcessToDb(processId, updatedProcess.current_process); +} +``` + +#### 6.5 Confirmation Automatique du Pairing +```typescript +// Check if this is a pairing process that's ready for confirmation +const existingDevice = await this.getDeviceFromDatabase(); +if (existingDevice && existingDevice.pairing_process_commitment === processId) { + const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1]; + if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) { + console.log('Pairing process updated with paired addresses, confirming pairing...'); + await this.confirmPairing(); + } +} +``` + +### 7. Confirmation du Pairing + +**Fonction :** `confirmPairing()` +**Localisation :** `src/services/service.ts:793-851` + +**Actions :** + +#### 7.1 Récupération du Processus de Pairing +```typescript +const existingDevice = await this.getDeviceFromDatabase(); +const pairingProcessId = existingDevice.pairing_process_commitment; +const myPairingProcess = await this.getProcess(pairingProcessId); +``` + +#### 7.2 Extraction des Adresses Couplées +```typescript +let myPairingState = this.getLastCommitedState(myPairingProcess); +const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; +const spAddressList = this.decodeValue(encodedSpAddressList); +``` + +#### 7.3 Pairing Effectif du Device +```typescript +this.sdkClient.unpair_device(); // Clear any existing pairing +this.sdkClient.pair_device(pairingProcessId, spAddressList); +``` + +#### 7.4 Sauvegarde du Device Mis à Jour +```typescript +const newDevice = this.dumpDeviceFromMemory(); +newDevice.pairing_process_commitment = pairingProcessId; +await this.saveDeviceInDatabase(newDevice); +``` + +## Architecture Technique + +### Côté Client (TypeScript) + +**Composants principaux :** +- **Interface Utilisateur :** Gestion des événements de clic et affichage des emojis +- **Services :** Orchestration du workflow et communication avec WebAssembly +- **Base de Données :** Stockage des devices, secrets et processus +- **WebSocket :** Communication avec les relais + +### Côté WebAssembly (Rust) + +**Fonctionnalités clés :** +- **Gestion des Wallets :** Création et manipulation des portefeuilles Silent Payment +- **Cryptographie :** Génération de secrets partagés et signatures +- **Transactions :** Création et finalisation des transactions Bitcoin +- **Processus :** Gestion des états et validation des changements + +## Types de Données Importants + +### Device +```typescript +interface Device { + sp_wallet: SpWallet; + pairing_process_commitment: OutPoint | null; + paired_member: Member; +} +``` + +### Process +```typescript +interface Process { + states: ProcessState[]; +} +``` + +### ApiReturn +```typescript +interface ApiReturn { + secrets: SecretsStore | null; + updated_process: UpdatedProcess | null; + new_tx_to_send: NewTxMessage | null; + ciphers_to_send: string[]; + commit_to_send: CommitMessage | null; + push_to_storage: string[]; + partial_tx: TsUnsignedTransaction | null; +} +``` + +## Flux de Communication + +1. **UI → Service :** Déclenchement du processus via `onCreateButtonClick()` +2. **Service → WebAssembly :** Appel des fonctions WASM pour la création du processus +3. **WebAssembly → Service :** Retour des données via `ApiReturn` +4. **Service → WebSocket :** Envoi des messages aux relais +5. **WebSocket → Service :** Réception des réponses des relais +6. **Service → Database :** Sauvegarde des états et secrets +7. **Service → UI :** Mise à jour de l'interface utilisateur + +## Points d'Attention + +### Sécurité +- Les secrets partagés sont générés côté WebAssembly (Rust) +- Les transactions sont signées de manière sécurisée +- Les données sensibles sont chiffrées avant stockage + +### Asynchronisme +- Le processus est entièrement asynchrone +- Utilisation de promesses et callbacks pour la coordination +- Gestion d'erreur à chaque étape critique + +### État du Processus +- Suivi de l'état via `processId` et `stateId` +- Sauvegarde persistante en base de données +- Récupération possible en cas d'interruption + +## Conclusion + +Le workflow de pairing représente un processus complexe mais bien structuré qui coordonne l'interface utilisateur TypeScript avec les fonctionnalités cryptographiques Rust via WebAssembly. Cette architecture permet une sécurité maximale tout en maintenant une expérience utilisateur fluide. Le processus est conçu pour être résilient aux interruptions et permet une récupération d'état en cas de problème. + +La séparation claire entre les responsabilités (UI, orchestration, cryptographie) facilite la maintenance et les évolutions futures du système de pairing. diff --git a/src/pages/signature/signature-component.ts b/src/pages/signature/signature-component.ts index 39e91b9..db1473a 100644 --- a/src/pages/signature/signature-component.ts +++ b/src/pages/signature/signature-component.ts @@ -1,5 +1,5 @@ import { SignatureElement } from './signature'; -import signatureCss from '../../../public/style/signature.css?raw' +import signatureCss from '../../../style/signature.css?raw' import Services from '../../services/service.js' class SignatureComponent extends HTMLElement { diff --git a/src/pages/signature/signature.ts b/src/pages/signature/signature.ts index f5b60fa..79b543a 100755 --- a/src/pages/signature/signature.ts +++ b/src/pages/signature/signature.ts @@ -1,4 +1,4 @@ -import signatureStyle from '../../../public/style/signature.css?inline'; +import signatureStyle from '../../../style/signature.css?inline'; declare global { interface Window { diff --git a/src/router.ts b/src/router.ts index d120232..48a6a24 100755 --- a/src/router.ts +++ b/src/router.ts @@ -1,17 +1,22 @@ -import '../public/style/4nk.css'; +// @ts-nocheck + +import './4nk.css'; import { initHeader } from '../src/components/header/header'; /*import { initChat } from '../src/pages/chat/chat';*/ import Database from './services/database.service'; import Services from './services/service'; import TokenService from './services/token'; import { cleanSubscriptions } from './utils/subscription.utils'; -import { LoginComponent } from './pages/home/home-component'; import { prepareAndSendPairingTx } from './utils/sp-address.utils'; import ModalService from './services/modal.service'; import { MessageType } from './models/process.model'; import { splitPrivateData, isValid32ByteHex } from './utils/service.utils'; import { MerkleProofResult } from 'pkg/sdk_client'; +// =================================================================================== +// ## 🧭 1. Routage de Page (Navigation) +// =================================================================================== + const routes: { [key: string]: string } = { home: '/src/pages/home/home.html', process: '/src/pages/process/process.html', @@ -24,108 +29,166 @@ const routes: { [key: string]: string } = { export let currentRoute = ''; export async function navigate(path: string) { + console.log(`[Router] 🧭 Navigation vers: ${path}`); cleanSubscriptions(); cleanPage(); - path = path.replace(/^\//, ''); + path = path.replace(/^\//, ''); // Retire le slash de début + + // Gère les chemins simples ou avec paramètres (ex: 'process-element/123_456') if (path.includes('/')) { const parsedPath = path.split('/')[0]; if (!routes[parsedPath]) { + console.warn(`[Router] ⚠️ Route inconnue "${parsedPath}", redirection vers 'home'.`); path = 'home'; } + } else if (!routes[path]) { + console.warn(`[Router] ⚠️ Route inconnue "${path}", redirection vers 'home'.`); + path = 'home'; } await handleLocation(path); } async function handleLocation(path: string) { + // 1. Log de démarrage + console.log(`[Router:handleLocation] 🧭 Gestion de la nouvelle route: ${path}`); + const parsedPath = path.split('/'); - if (path.includes('/')) { - path = parsedPath[0]; - } - currentRoute = path; - const routeHtml = routes[path] || routes['home']; + const baseRoute = parsedPath[0]; + currentRoute = baseRoute; + const routeHtml = routes[baseRoute] || routes['home']; const content = document.getElementById('containerId'); - if (content) { - if (path === 'home') { - const login = LoginComponent; - const container = document.querySelector('#containerId'); - const accountComponent = document.createElement('login-4nk-component'); - accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;'); - if (container) container.appendChild(accountComponent); - } else if (path !== 'process') { - const html = await fetch(routeHtml).then((data) => data.text()); - content.innerHTML = html; + if (!content) { + console.error('[Router] 💥 Erreur critique: div #containerId non trouvée !'); + return; + } else { + // console.debug('[Router:handleLocation] ✅ conteneur #containerId trouvé.'); + } + + // --- Injection de Contenu --- + if (baseRoute === 'home') { + console.log('[Router:handleLocation] 🏠 Route "home" détectée. Importation dynamique de ...'); + + const { LoginComponent } = await import('./pages/home/home-component'); + + if (!customElements.get('login-4nk-component')) { + customElements.define('login-4nk-component', LoginComponent); + console.log('[Router:handleLocation] ℹ️ défini.'); } - await new Promise(requestAnimationFrame); - injectHeader(); + const container = document.querySelector('#containerId'); + const accountComponent = document.createElement('login-4nk-component'); + accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;'); + if (container) { + container.appendChild(accountComponent); + // 4. Log de succès (le plus important pour ton bug) + console.log('[Router:handleLocation] ✅ ajouté au DOM.'); + } + } else if (baseRoute !== 'process') { + // 2. Log pour les autres pages + console.log(`[Router:handleLocation] 📄 Fetching et injection de HTML pour: ${routeHtml}`); + const html = await fetch(routeHtml).then((data) => data.text()); + content.innerHTML = html; + console.log(`[Router:handleLocation] ✅ HTML pour "${baseRoute}" injecté.`); + } // (Le cas 'process' est géré plus bas dans le switch) + await new Promise(requestAnimationFrame); + console.log('[Router:handleLocation] 💉 Injection du header...'); + injectHeader(); // --- Logique Spécifique à la Page (Lazy Loading) --- - // const modalService = await ModalService.getInstance() - // modalService.injectValidationModal() - switch (path) { - case 'process': - // const { init } = await import('./pages/process/process'); - //const { ProcessListComponent } = await import('./pages/process/process-list-component'); + console.log(`[Router] Initialisation de la logique pour la route: ${baseRoute}`); + switch (baseRoute) { + case 'process': + console.log('[Router:switch] 📦 Chargement de ProcessListComponent...'); + const { ProcessListComponent } = await import('./pages/process/process-list-component'); + const container2 = document.querySelector('#containerId'); + const processListComponent = document.createElement('process-list-4nk-component'); - const container2 = document.querySelector('#containerId'); - const accountComponent = document.createElement('process-list-4nk-component'); + if (!customElements.get('process-list-4nk-component')) { + console.log('[Router:switch] ℹ️ Définition de ...'); + customElements.define('process-list-4nk-component', ProcessListComponent); + } + processListComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;'); + if (container2) { + container2.appendChild(processListComponent); + console.log('[Router:switch] ✅ ajouté au DOM.'); + } + break; - //if (!customElements.get('process-list-4nk-component')) { - //customElements.define('process-list-4nk-component', ProcessListComponent); - //} - accountComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;'); - if (container2) container2.appendChild(accountComponent); - break; - - case 'process-element': - if (parsedPath && parsedPath.length) { - const { initProcessElement } = await import('./pages/process-element/process-element'); - const parseProcess = parsedPath[1].split('_'); - initProcessElement(parseProcess[0], parseProcess[1]); + case 'process-element': + console.log(`[Router:switch] 📦 Chargement de ...`); + if (parsedPath[1]) { + // 1. Importe le composant (juste pour être sûr qu'il est défini) + const { ProcessElementComponent } = await import('./pages/process-element/process-component'); + if (!customElements.get('process-4nk-component')) { + customElements.define('process-4nk-component', ProcessElementComponent); } - break; - case 'account': - const { AccountComponent } = await import('./pages/account/account-component'); - const accountContainer = document.querySelector('.parameter-list'); - if (accountContainer) { - if (!customElements.get('account-component')) { - customElements.define('account-component', AccountComponent); - } - const accountComponent = document.createElement('account-component'); - accountContainer.appendChild(accountComponent); - } - break; + // 2. Sépare les IDs + const [processId, stateId] = parsedPath[1].split('_'); - /*case 'chat': - const { ChatComponent } = await import('./pages/chat/chat-component'); - const chatContainer = document.querySelector('.group-list'); - if (chatContainer) { - if (!customElements.get('chat-component')) { - customElements.define('chat-component', ChatComponent); - } - const chatComponent = document.createElement('chat-component'); - chatContainer.appendChild(chatComponent); - } - break;*/ + // 3. Crée l'élément et passe les attributs + const container = document.querySelector('#containerId'); + const processElement = document.createElement('process-4nk-component'); + processElement.setAttribute('process-id', processId); + processElement.setAttribute('state-id', stateId); - case 'signature': - const { SignatureComponent } = await import('./pages/signature/signature-component'); - const container = document.querySelector('.group-list'); if (container) { - if (!customElements.get('signature-component')) { - customElements.define('signature-component', SignatureComponent); - } - const signatureComponent = document.createElement('signature-component'); - container.appendChild(signatureComponent); + container.appendChild(processElement); + console.log(`[Router:switch] ✅ ajouté au DOM pour ${processId}_${stateId}`); } - break; - } + } else { + console.error('[Router] 💥 Route process-element appelée sans ID (ex: process-element/processId_stateId)'); + navigate('process'); + } + break; + + case 'account': + console.log('[Router:switch] 📦 Chargement de AccountComponent...'); + const { AccountComponent } = await import('./pages/account/account-component'); + const accountContainer = document.querySelector('.parameter-list'); + if (accountContainer) { + if (!customElements.get('account-component')) { + console.log('[Router:switch] ℹ️ Définition de ...'); + customElements.define('account-component', AccountComponent); + } + const accountComponent = document.createElement('account-component'); + accountContainer.appendChild(accountComponent); + console.log('[Router:switch] ✅ ajouté au DOM.'); + } else { + console.warn('[Router:switch] ⚠️ Impossible de trouver ".parameter-list" pour injecter AccountComponent.'); + } + break; + + case 'signature': + console.log('[Router:switch] 📦 Chargement de SignatureComponent...'); + const { SignatureComponent } = await import('./pages/signature/signature-component'); + const container = document.querySelector('.group-list'); + if (container) { + if (!customElements.get('signature-component')) { + console.log('[Router:switch] ℹ️ Définition de ...'); + customElements.define('signature-component', SignatureComponent); + } + const signatureComponent = document.createElement('signature-component'); + container.appendChild(signatureComponent); + console.log('[Router:switch] ✅ ajouté au DOM.'); + } else { + console.warn('[Router:switch] ⚠️ Impossible de trouver ".group-list" pour injecter SignatureComponent.'); + } + break; + + // Log pour les cas non gérés + case 'home': + console.log('[Router:switch] ℹ️ Route "home". La logique a déjà été gérée avant le switch.'); + break; + + default: + console.log(`[Router:switch] ℹ️ Route "${baseRoute}" n'a pas de logique d'initialisation spécifique dans le switch.`); } } window.onpopstate = async () => { + console.log('[Router] 🔄 Changement d\'état "popstate" (bouton retour/avant)'); const services = await Services.getInstance(); if (!services.isPaired()) { handleLocation('home'); @@ -134,68 +197,100 @@ window.onpopstate = async () => { } }; +// --- Fin de la section Routage de Page --- +// =================================================================================== + +// =================================================================================== +// ## 🚀 2. Initialisation de l'Application +// =================================================================================== + export async function init(): Promise { + console.log("[Router:Init] 🚀 Démarrage de l'application..."); try { const services = await Services.getInstance(); - (window as any).myService = services; + (window as any).myService = services; // Pour débogage manuel + + console.log('[Router:Init] 📦 Initialisation de la base de données (IndexedDB)...'); const db = await Database.getInstance(); db.registerServiceWorker('/src/service-workers/database.worker.js'); + + console.log("[Router:Init] 📱 Vérification de l'appareil (device)..."); const device = await services.getDeviceFromDatabase(); - console.log('🚀 ~ setTimeout ~ device:', device); + console.log('🚀 ~ setTimeout ~ device:', device); // Log original gardé if (!device) { + console.log("[Router:Init] ✨ Aucun appareil trouvé. Création d'un nouvel appareil..."); await services.createNewDevice(); } else { + console.log("[Router:Init] 🔄 Restauration de l'appareil depuis la BDD..."); services.restoreDevice(device); } - - // If we create a new device, we most probably don't have anything in db, but just in case + + console.log("[Router:Init] 💾 Restauration de l'état (processus et secrets) depuis la BDD..."); await services.restoreProcessesFromDB(); await services.restoreSecretsFromDB(); - // We connect to all relays now + console.log('[Router:Init] 🔌 Connexion à tous les relais...'); await services.connectAllRelays(); - // We register all the event listeners if we run in an iframe + // S'enregistre comme "serveur" API si nous sommes dans une iframe if (window.self !== window.top) { + console.log('[Router:Init] 📡 Nous sommes dans une iframe. Enregistrement des listeners API...'); await registerAllListeners(); + } else { + console.log('[Router:Init] ℹ️ Exécution en mode standalone (pas dans une iframe).'); } + console.log("[Router:Init] 🧭 Vérification du statut d'appairage pour la navigation..."); if (services.isPaired()) { + console.log('[Router:Init] ✅ Appairé. Navigation vers "process".'); await navigate('process'); } else { + console.log('[Router:Init] ❌ Non appairé. Navigation vers "home".'); await navigate('home'); } } catch (error) { - console.error(error); + console.error("[Router:Init] 💥 ERREUR CRITIQUE PENDANT L'INITIALISATION:", error); await navigate('home'); } } +// --- Fin de la section Initialisation --- +// =================================================================================== + +// =================================================================================== +// ## 📡 3. API (Message Listeners pour Iframe) +// =================================================================================== + export async function registerAllListeners() { + console.log('[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)...'); const services = await Services.getInstance(); const tokenService = await TokenService.getInstance(); - const errorResponse = (errorMsg: string, origin: string, messageId?: string) => { + /** + * Fonction centralisée pour envoyer des réponses d'erreur à la fenêtre parente (l'application A). + */ + const errorResponse = (errorMsg: string, origin: string, messageId?: string) => { + console.error(`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`); window.parent.postMessage( - { - type: MessageType.ERROR, + { + type: MessageType.ERROR, error: errorMsg, - messageId + messageId, }, - origin + origin, ); - } + }; + + // --- Définitions des gestionnaires (Handlers) --- - // --- Handler functions --- const handleRequestLink = async (event: MessageEvent) => { - if (event.data.type !== MessageType.REQUEST_LINK) { - return; - } + console.log(`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`); const modalService = await ModalService.getInstance(); - const result = await modalService.showConfirmationModal({ - title: 'Confirmation de liaison', - content: ` + const result = await modalService.showConfirmationModal( + { + title: 'Confirmation de liaison', + content: ` `, - confirmText: 'Ajouter un service', - cancelText: 'Annuler' - }, true); + confirmText: 'Ajouter un service', + cancelText: 'Annuler', + }, + true, + ); if (!result) { - const errorMsg = 'Failed to pair device: User refused to link'; - errorResponse(errorMsg, event.origin, event.data.messageId); + throw new Error('Failed to pair device: User refused to link'); } - try { - const tokens = await tokenService.generateSessionToken(event.origin); - const acceptedMsg = { - type: MessageType.LINK_ACCEPTED, + const tokens = await tokenService.generateSessionToken(event.origin); + window.parent.postMessage( + { + type: MessageType.LINK_ACCEPTED, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, - messageId: event.data.messageId - }; - window.parent.postMessage( - acceptedMsg, - event.origin - ); - } catch (error) { - const errorMsg = `Failed to generate tokens: ${error}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } + messageId: event.data.messageId, + }, + event.origin, + ); + console.log(`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`); + }; const handleCreatePairing = async (event: MessageEvent) => { - if (event.data.type !== MessageType.CREATE_PAIRING) { - return; - } - - console.log("📨 [Router] Received CREATE_PAIRING request"); - + console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`); + if (services.isPaired()) { - const errorMsg = "⚠️ Device already paired — ignoring CREATE_PAIRING request"; - console.warn(errorMsg); - errorResponse(errorMsg, event.origin, event.data.messageId); - return; + throw new Error('Device already paired — ignoring CREATE_PAIRING request'); } - - try { - const { accessToken } = event.data; - - console.log("🔐 Checking access token validity..."); - const validToken = accessToken && (await tokenService.validateToken(accessToken, event.origin)); - - if (!validToken) { - throw new Error("❌ Invalid or expired session token"); - } - - console.log("✅ Token validated successfully"); - console.log("🚀 Starting pairing process"); - - const myAddress = services.getDeviceAddress(); - console.log("📍 Device address:", myAddress); - - console.log("🧱 Creating pairing process..."); - const createPairingProcessReturn = await services.createPairingProcess("", [myAddress]); - console.log("🧾 Pairing process created:", createPairingProcessReturn); - - const pairingId = createPairingProcessReturn.updated_process?.process_id; - const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string; - - console.log("🔗 Pairing ID:", pairingId); - console.log("🧩 State ID:", stateId); - - console.log("🔒 Registering device as paired..."); - services.pairDevice(pairingId, [myAddress]); - - console.log("🧠 Handling API return for createPairingProcess..."); - await services.handleApiReturn(createPairingProcessReturn); - - console.log("🧰 Creating PRD update..."); - const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId); - console.log("🧾 PRD update result:", createPrdUpdateReturn); - await services.handleApiReturn(createPrdUpdateReturn); - - console.log("✅ Approving change..."); - const approveChangeReturn = await services.approveChange(pairingId, stateId); - console.log("📜 Approve change result:", approveChangeReturn); - await services.handleApiReturn(approveChangeReturn); - - console.log("🔁 Confirming pairing..."); - await services.confirmPairing(); - - console.log("🎉 Pairing successfully completed!"); - - // ✅ Send success response to frontend - const successMsg = { - type: MessageType.PAIRING_CREATED, - pairingId, - messageId: event.data.messageId - }; - console.log("📤 Sending PAIRING_CREATED message to UI:", successMsg); - window.parent.postMessage(successMsg, event.origin); - - } catch (e) { - const errorMsg = `❌ Failed to create pairing process: ${e}`; - console.error(errorMsg); - errorResponse(errorMsg, event.origin, event.data.messageId); + + const { accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } + + console.log("[Router:API] 🚀 Démarrage du processus d'appairage..."); + + const myAddress = services.getDeviceAddress(); + console.log('[Router:API] 1/7: Création du processus de pairing...'); + const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]); + + const pairingId = createPairingProcessReturn.updated_process?.process_id; + const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string; + if (!pairingId || !stateId) { + throw new Error('Pairing process creation failed to return valid IDs'); + } + console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`); + + console.log("[Router:API] 3/7: Enregistrement local de l'appareil..."); + services.pairDevice(pairingId, [myAddress]); + + console.log('[Router:API] 4/7: Traitement du retour (handleApiReturn)...'); + await services.handleApiReturn(createPairingProcessReturn); + + console.log('[Router:API] 5/7: Création de la mise à jour PRD...'); + const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId); + await services.handleApiReturn(createPrdUpdateReturn); + + console.log('[Router:API] 6/7: Approbation du changement...'); + const approveChangeReturn = await services.approveChange(pairingId, stateId); + await services.handleApiReturn(approveChangeReturn); + + console.log('[Router:API] 7/7: Confirmation finale du pairing...'); + await services.confirmPairing(); + + console.log('[Router:API] 🎉 Appairage terminé avec succès !'); + + const successMsg = { + type: MessageType.PAIRING_CREATED, + pairingId, + messageId: event.data.messageId, + }; + window.parent.postMessage(successMsg, event.origin); }; const handleGetMyProcesses = async (event: MessageEvent) => { - if (event.data.type !== MessageType.GET_MY_PROCESSES) { - return; + console.log(`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - errorResponse(errorMsg, event.origin, event.data.messageId); - return; - } + const myProcesses = await services.getMyProcesses(); - try { - const { accessToken } = event.data; + window.parent.postMessage( + { + type: MessageType.GET_MY_PROCESSES, + myProcesses, + messageId: event.data.messageId, + }, + event.origin, + ); + }; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const myProcesses = await services.getMyProcesses(); - - window.parent.postMessage( - { - type: MessageType.GET_MY_PROCESSES, - myProcesses, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to get processes: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } - const handleGetProcesses = async (event: MessageEvent) => { - if (event.data.type !== MessageType.GET_PROCESSES) { - return; + console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - const tokenService = await TokenService.getInstance(); + const processes = await services.getProcesses(); - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - errorResponse(errorMsg, event.origin, event.data.messageId); - return; - } + window.parent.postMessage( + { + type: MessageType.PROCESSES_RETRIEVED, + processes, + messageId: event.data.messageId, + }, + event.origin, + ); + }; - try { - const { accessToken } = event.data; - - // Validate the session token - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const processes = await services.getProcesses(); - - window.parent.postMessage( - { - type: MessageType.PROCESSES_RETRIEVED, - processes, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to get processes: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } - - /// We got a state for some process and return as many clear attributes as we can const handleDecryptState = async (event: MessageEvent) => { - if (event.data.type !== MessageType.RETRIEVE_DATA) { - return; - } - const tokenService = await TokenService.getInstance(); + console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - errorResponse(errorMsg, event.origin, event.data.messageId); - return; + const { processId, stateId, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - try { - const { processId, stateId, accessToken } = event.data; + const process = await services.getProcess(processId); + if (!process) throw new Error("Can't find process"); - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); + const state = services.getStateFromId(process, stateId); + if (!state) throw new Error(`Unknown state ${stateId} for process ${processId}`); + + console.log(`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}:${stateId}`); + await services.ensureConnections(process, stateId); + + const res: Record = {}; + for (const attribute of Object.keys(state.pcd_commitment)) { + if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) { + continue; } - - // Retrieve the state for the process - const process = await services.getProcess(processId); - if (!process) { - throw new Error("Can't find process"); + const decryptedAttribute = await services.decryptAttribute(processId, state, attribute); + if (decryptedAttribute) { + res[attribute] = decryptedAttribute; } - const state = services.getStateFromId(process, stateId); - - await services.checkConnections(process, stateId); - - const res: Record = {}; - if (state) { - // Decrypt all the data we have the key for - for (const attribute of Object.keys(state.pcd_commitment)) { - if (attribute === 'roles' || state.public_data[attribute]) { - continue; - } - const decryptedAttribute = await services.decryptAttribute(processId, state, attribute); - if (decryptedAttribute) { - res[attribute] = decryptedAttribute; - } - } - } else { - throw new Error('Unknown state for process', processId); - } - - window.parent.postMessage( - { - type: MessageType.DATA_RETRIEVED, - data: res, - messageId: event.data.messageId, - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to retrieve data: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); } - } + console.log(`[Router:API] ✅ Déchiffrement terminé pour ${processId}:${stateId}. ${Object.keys(res).length} attribut(s) déchiffré(s).`); + + window.parent.postMessage( + { + type: MessageType.DATA_RETRIEVED, + data: res, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleValidateToken = async (event: MessageEvent) => { - if (event.data.type !== MessageType.VALIDATE_TOKEN) { - return; - } - + console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_TOKEN} reçu`); const accessToken = event.data.accessToken; const refreshToken = event.data.refreshToken; if (!accessToken || !refreshToken) { - errorResponse('Failed to validate token: missing access, refresh token or both', event.origin, event.data.messageId); + throw new Error('Missing access, refresh token or both'); } const isValid = await tokenService.validateToken(accessToken, event.origin); - + console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`); window.parent.postMessage( { type: MessageType.VALIDATE_TOKEN, accessToken: accessToken, refreshToken: refreshToken, isValid: isValid, - messageId: event.data.messageId + messageId: event.data.messageId, }, - event.origin + event.origin, ); }; const handleRenewToken = async (event: MessageEvent) => { - if (event.data.type !== MessageType.RENEW_TOKEN) { - return; - } - - try { - const refreshToken = event.data.refreshToken; - - if (!refreshToken) { - throw new Error('No refresh token provided'); - } - - const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin); - - if (!newAccessToken) { - throw new Error('Failed to refresh token'); - } - - window.parent.postMessage( - { - type: MessageType.RENEW_TOKEN, - accessToken: newAccessToken, - refreshToken: refreshToken, - messageId: event.data.messageId - }, - event.origin - ); - } catch (error) { - const errorMsg = `Failed to renew token: ${error}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } + console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`); + const refreshToken = event.data.refreshToken; + if (!refreshToken) throw new Error('No refresh token provided'); + + const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin); + if (!newAccessToken) throw new Error('Failed to refresh token (invalid refresh token)'); + + console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`); + window.parent.postMessage( + { + type: MessageType.RENEW_TOKEN, + accessToken: newAccessToken, + refreshToken: refreshToken, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleGetPairingId = async (event: MessageEvent) => { - if (event.data.type !== MessageType.GET_PAIRING_ID) return; + console.log(`[Router:API] 📨 Message ${MessageType.GET_PAIRING_ID} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - console.log('Device not paired - running diagnosis...'); - await services.diagnosePairingState(); - errorResponse(errorMsg, event.origin, event.data.messageId); - return; + const { accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - try { - const { accessToken } = event.data; - - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const userPairingId = services.getPairingProcessId(); - - window.parent.postMessage( - { - type: MessageType.GET_PAIRING_ID, - userPairingId, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to get pairing id: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } + const userPairingId = services.getPairingProcessId(); + window.parent.postMessage( + { + type: MessageType.GET_PAIRING_ID, + userPairingId, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleCreateProcess = async (event: MessageEvent) => { - if (event.data.type !== MessageType.CREATE_PROCESS) return; + console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - errorResponse(errorMsg, event.origin, event.data.messageId); - return; + const { processData, privateFields, roles, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - try { - const { processData, privateFields, roles, accessToken } = event.data; + console.log('[Router:API] 🚀 Démarrage de la création de processus standard...'); + const { privateData, publicData } = splitPrivateData(processData, privateFields); - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const { privateData, publicData } = splitPrivateData(processData, privateFields); - - const createProcessReturn = await services.createProcess(privateData, publicData, roles); - if (!createProcessReturn.updated_process) { - throw new Error('Empty updated_process in createProcessReturn'); - } - const processId = createProcessReturn.updated_process.process_id; - const process = createProcessReturn.updated_process.current_process; - const stateId = process.states[0].state_id; - await services.handleApiReturn(createProcessReturn); - - const res = { - processId, - process, - processData, - } - - window.parent.postMessage( - { - type: MessageType.PROCESS_CREATED, - processCreated: res, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to create process: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); + // --- 1. CRÉATION DU PROCESSUS --- + console.log('[Router:API] 1/4: Création du processus...'); + const createProcessReturn = await services.createProcess(privateData, publicData, roles); + if (!createProcessReturn.updated_process) { + throw new Error('Empty updated_process in createProcessReturn'); } - } + + const processId = createProcessReturn.updated_process.process_id; + const process = createProcessReturn.updated_process.current_process; + const stateId = process.states[0].state_id; + console.log(`[Router:API] 2/4: Processus ${processId} créé. Traitement...`); + await services.handleApiReturn(createProcessReturn); + + // --- 2. MISE À JOUR PRD --- + console.log('[Router:API] 3/4: Création de la mise à jour PRD...'); + const createPrdUpdateReturn = await services.createPrdUpdate(processId, stateId); + await services.handleApiReturn(createPrdUpdateReturn); + + // --- 3. APPROBATION DU CHANGEMENT --- + console.log('[Router:API] 4/4: Approbation du changement...'); + const approveChangeReturn = await services.approveChange(processId, stateId); + await services.handleApiReturn(approveChangeReturn); + + console.log(`[Router:API] 🎉 Processus ${processId} créé et auto-approuvé.`); + + const res = { + processId, + process, + processData, + }; + + window.parent.postMessage( + { + type: MessageType.PROCESS_CREATED, + processCreated: res, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleNotifyUpdate = async (event: MessageEvent) => { - if (event.data.type !== MessageType.NOTIFY_UPDATE) return; + console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - errorResponse(errorMsg, event.origin, event.data.messageId); - return; + const { processId, stateId, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } + if (!isValid32ByteHex(stateId)) throw new Error('Invalid state id'); - try { - const { processId, stateId, accessToken } = event.data; + const res = await services.createPrdUpdate(processId, stateId); + await services.handleApiReturn(res); - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - if (!isValid32ByteHex(stateId)) { - throw new Error('Invalid state id'); - } - - const res = await services.createPrdUpdate(processId, stateId); - await services.handleApiReturn(res); - - window.parent.postMessage( - { - type: MessageType.UPDATE_NOTIFIED, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to notify update for process: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } + window.parent.postMessage( + { + type: MessageType.UPDATE_NOTIFIED, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleValidateState = async (event: MessageEvent) => { - if (event.data.type !== MessageType.VALIDATE_STATE) return; + console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - errorResponse(errorMsg, event.origin, event.data.messageId); - return; + const { processId, stateId, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - try { - const { processId, stateId, accessToken } = event.data; + const res = await services.approveChange(processId, stateId); + await services.handleApiReturn(res); - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } + window.parent.postMessage( + { + type: MessageType.STATE_VALIDATED, + validatedProcess: res.updated_process, + messageId: event.data.messageId, + }, + event.origin, + ); + }; - const res = await services.approveChange(processId, stateId); - await services.handleApiReturn(res); - - window.parent.postMessage( - { - type: MessageType.STATE_VALIDATED, - validatedProcess: res.updated_process, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to validate process: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } - const handleUpdateProcess = async (event: MessageEvent) => { - if (event.data.type !== MessageType.UPDATE_PROCESS) return; + console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - errorResponse(errorMsg, event.origin, event.data.messageId); + const { processId, newData, privateFields, roles, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - try { - // privateFields is only used if newData contains new fields - // roles can be empty meaning that roles from the last commited state are kept - const { processId, newData, privateFields, roles, accessToken } = event.data; + const process = await services.getProcess(processId); + if (!process) throw new Error('Process not found'); - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); + // --- Logique complexe de gestion d'état --- + // (Cette logique est très dense et pourrait être déplacée dans le service) + console.log(`[Router:API] 🔄 Calcul de la diff pour la mise à jour de ${processId}...`); + let lastState = services.getLastCommitedState(process); + if (!lastState) { + console.warn(`[Router:API] ⚠️ Processus ${processId} n'a pas d'état "commited". Tentative d'auto-approbation du 1er état...`); + const firstState = process.states[0]; + const roles = firstState.roles; + if (services.rolesContainsUs(roles)) { + const approveChangeRes = await services.approveChange(processId, firstState.state_id); + await services.handleApiReturn(approveChangeRes); + const prdUpdateRes = await services.createPrdUpdate(processId, firstState.state_id); + await services.handleApiReturn(prdUpdateRes); + } else { + if (firstState.validation_tokens.length > 0) { + const res = await services.createPrdUpdate(processId, firstState.state_id); + await services.handleApiReturn(res); + } } - - // Check if the new data is already in the process or if it's a new field - const process = await services.getProcess(processId); - if (!process) { - throw new Error('Process not found'); - } - let lastState = services.getLastCommitedState(process); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Attente arbitraire + lastState = services.getLastCommitedState(process); if (!lastState) { - const firstState = process.states[0]; - const roles = firstState.roles; - if (services.rolesContainsUs(roles)) { - const approveChangeRes= await services.approveChange(processId, firstState.state_id); - await services.handleApiReturn(approveChangeRes); - const prdUpdateRes = await services.createPrdUpdate(processId, firstState.state_id); - await services.handleApiReturn(prdUpdateRes); - } else { - if (firstState.validation_tokens.length > 0) { - // Try to send it again anyway - const res = await services.createPrdUpdate(processId, firstState.state_id); - await services.handleApiReturn(res); - } - } - // Wait a couple seconds - await new Promise(resolve => setTimeout(resolve, 2000)); - lastState = services.getLastCommitedState(process); - if (!lastState) { - throw new Error('Process doesn\'t have a commited state yet'); - } + throw new Error("Process doesn't have a commited state yet"); } - const lastStateIndex = services.getLastCommitedStateIndex(process); - if (lastStateIndex === null) { - throw new Error('Process doesn\'t have a commited state yet'); - } // Shouldn't happen - - const privateData: Record = {}; - const publicData: Record = {}; - - for (const field of Object.keys(newData)) { - // Public data are carried along each new state - // So the first thing we can do is check if the new data is public data - if (lastState.public_data[field]) { - // Add it to public data - publicData[field] = newData[field]; - continue; - } - - // If it's not a public data, it may be either a private data update, or a new field (public of private) - // Caller gave us a list of new private fields, if we see it here this is a new private field - if (privateFields.includes(field)) { - // Add it to private data - privateData[field] = newData[field]; - continue; - } - - // Now it can be an update of private data or a new public data - // We check that the field exists in previous states private data - for (let i = lastStateIndex; i >= 0; i--) { - const state = process.states[i]; - if (state.pcd_commitment[field]) { - // We don't even check if it's a public field, we would have seen it in the last state - privateData[field] = newData[field]; - break; - } else { - // This attribute was not modified in that state, we go back to the previous state - continue; - } - } - - if (privateData[field]) continue; - - // We've get back all the way to the first state without seeing it, it's a new public field - publicData[field] = newData[field]; - } - - // We'll let the wasm check if roles are consistent - - const res = await services.updateProcess(process, privateData, publicData, roles); - await services.handleApiReturn(res); - - window.parent.postMessage( - { - type: MessageType.PROCESS_UPDATED, - updatedProcess: res.updated_process, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to update process: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); } - } + const lastStateIndex = services.getLastCommitedStateIndex(process); + if (lastStateIndex === null) throw new Error("Process doesn't have a commited state yet"); + + 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; + } + for (let i = lastStateIndex; i >= 0; i--) { + const state = process.states[i]; + if (state.pcd_commitment[field]) { + privateData[field] = newData[field]; + break; + } else { + continue; + } + } + if (privateData[field]) continue; + publicData[field] = newData[field]; + } + console.log(`[Router:API] 🔄 Envoi de la mise à jour à services.updateProcess...`); + const res = await services.updateProcess(process, privateData, publicData, roles); + await services.handleApiReturn(res); + + window.parent.postMessage( + { + type: MessageType.PROCESS_UPDATED, + updatedProcess: res.updated_process, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleDecodePublicData = async (event: MessageEvent) => { - if (event.data.type !== MessageType.DECODE_PUBLIC_DATA) return; + console.log(`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); - if (!services.isPaired()) { - const errorMsg = 'Device not paired'; - errorResponse(errorMsg, event.origin, event.data.messageId); - return; + const { accessToken, encodedData } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - try { - const { accessToken, encodedData } = event.data; - - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const decodedData = services.decodeValue(encodedData); - - window.parent.postMessage( - { - type: MessageType.PUBLIC_DATA_DECODED, - decodedData, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to decode data: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } + const decodedData = services.decodeValue(encodedData); + window.parent.postMessage( + { + type: MessageType.PUBLIC_DATA_DECODED, + decodedData, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleHashValue = async (event: MessageEvent) => { - if (event.data.type !== MessageType.HASH_VALUE) return; - - console.log('handleHashValue', event.data); - - try { - const { accessToken, commitedIn, label, fileBlob } = event.data; - - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const hash = services.getHashForFile(commitedIn, label, fileBlob); - - window.parent.postMessage( - { - type: MessageType.VALUE_HASHED, - hash, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to hash value: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); + console.log(`[Router:API] 📨 Message ${MessageType.HASH_VALUE} reçu`); + const { accessToken, commitedIn, label, fileBlob } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - } + + const hash = services.getHashForFile(commitedIn, label, fileBlob); + window.parent.postMessage( + { + type: MessageType.VALUE_HASHED, + hash, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleGetMerkleProof = async (event: MessageEvent) => { - if (event.data.type !== MessageType.GET_MERKLE_PROOF) return; - - try { - const { accessToken, processState, attributeName } = event.data; - - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const proof = services.getMerkleProofForFile(processState, attributeName); - - window.parent.postMessage( - { - type: MessageType.MERKLE_PROOF_RETRIEVED, - proof, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to get merkle proof: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); + console.log(`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`); + const { accessToken, processState, attributeName } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - } + + const proof = services.getMerkleProofForFile(processState, attributeName); + window.parent.postMessage( + { + type: MessageType.MERKLE_PROOF_RETRIEVED, + proof, + messageId: event.data.messageId, + }, + event.origin, + ); + }; const handleValidateMerkleProof = async (event: MessageEvent) => { - if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF) return; - - try { - const { accessToken, merkleProof, documentHash } = event.data; - - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - // Try to parse the proof - // We will validate it's a MerkleProofResult in the wasm - let parsedMerkleProof: MerkleProofResult; - try { - parsedMerkleProof= JSON.parse(merkleProof); - } catch (e) { - throw new Error('Provided merkleProof is not a valid json object'); - } - - const res = services.validateMerkleProof(parsedMerkleProof, documentHash); - - window.parent.postMessage( - { - type: MessageType.MERKLE_PROOF_VALIDATED, - isValid: res, - messageId: event.data.messageId - }, - event.origin - ); - } catch (e) { - const errorMsg = `Failed to get merkle proof: ${e}`; - errorResponse(errorMsg, event.origin, event.data.messageId); + console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`); + const { accessToken, merkleProof, documentHash } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); } - } + + let parsedMerkleProof: MerkleProofResult; + try { + parsedMerkleProof = JSON.parse(merkleProof); + } catch (e) { + throw new Error('Provided merkleProof is not a valid json object'); + } + + const res = services.validateMerkleProof(parsedMerkleProof, documentHash); + window.parent.postMessage( + { + type: MessageType.MERKLE_PROOF_VALIDATED, + isValid: res, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + // --- Le "Switchyard" : il reçoit tous les messages et les dispatche --- window.removeEventListener('message', handleMessage); window.addEventListener('message', handleMessage); async function handleMessage(event: MessageEvent) { + // Le 'switch' est dans un 'try...catch' + // Grâce au 'await' sur chaque 'handle', si l'un d'eux échoue (throw), + // l'erreur sera interceptée ici et renvoyée proprement. try { + // AMÉLIORATION CRITIQUE: Ajout de 'await' à chaque appel + // pour que le try/catch puisse intercepter les erreurs. switch (event.data.type) { case MessageType.REQUEST_LINK: await handleRequestLink(event); @@ -909,6 +814,7 @@ export async function registerAllListeners() { await handleCreateProcess(event); break; case MessageType.CREATE_CONVERSATION: + // @ts-ignore - 'handleCreateConversationProcess' n'est pas défini dans le fichier await handleCreateConversationProcess(event); break; case MessageType.NOTIFY_UPDATE: @@ -933,22 +839,27 @@ export async function registerAllListeners() { await handleValidateMerkleProof(event); break; default: - console.warn(`Unhandled message type: ${event.data.type}`); + console.warn(`[Router:API] ⚠️ Message non géré reçu: ${event.data.type}`); } } catch (error) { - const errorMsg = `Error handling message: ${error}`; + // C'est le "filet de sécurité" global + const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error.message || error}`; errorResponse(errorMsg, event.origin, event.data.messageId); } } + // Notifie l'application parente que l'Iframe (le "serveur") est prête window.parent.postMessage( { - type: MessageType.LISTENING + type: MessageType.LISTENING, }, - '*' + '*', ); + console.log('[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent.'); } +// --- Fonctions utilitaires de la page --- + async function cleanPage() { const container = document.querySelector('#containerId'); if (container) container.innerHTML = ''; @@ -970,17 +881,26 @@ async function injectHeader() { (window as any).navigate = navigate; -document.addEventListener('navigate', ((e: Event) => { - const event = e as CustomEvent<{page: string, processId?: string}>; +// Gère les événements de navigation personnalisés (ex: depuis le header) +document.addEventListener('navigate', (e: Event) => { + const event = e as CustomEvent<{ page: string; processId?: string }>; + console.log(`[Router] 🧭 Événement de navigation personnalisé reçu: ${event.detail.page}`); if (event.detail.page === 'chat') { + // Logique spécifique pour 'chat' const container = document.querySelector('.container'); if (container) container.innerHTML = ''; - + //initChat(); - + const chatElement = document.querySelector('chat-element'); if (chatElement) { chatElement.setAttribute('process-id', event.detail.processId || ''); } + } else { + // Gère les autres navigations personnalisées + navigate(event.detail.page); } -})); +}); + +// --- Fin de la section API --- +// =================================================================================== diff --git a/src/services/service.ts b/src/services/service.ts index b797ba4..129da43 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1,10 +1,11 @@ +// @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 { navigate } from '../router'; import { storeData, retrieveData, testData } from './storage.service'; import { BackUp } from '~/models/backup.model'; @@ -52,9 +53,10 @@ export default class Services { })(); } - console.log('initializing services'); + 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; } @@ -88,24 +90,28 @@ export default class Services { * 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(`Connecting to: ${wsurl}`); - await this.addWebsocketConnection(wsurl); - connectedUrls.push(wsurl); - console.log(`Successfully connected to: ${wsurl}`); - } catch (error) { - console.error(`Failed to connect to ${wsurl}:`, error); - } + 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) { - await this.waitForHandshakeMessage(); + } + + // 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 { @@ -116,7 +122,7 @@ export default class Services { } return this.relayReadyPromise; } - + private resolveRelayReady(): void { if (this.relayReadyResolver) { this.relayReadyResolver(); @@ -126,7 +132,7 @@ export default class Services { } public async addWebsocketConnection(url: string): Promise { - console.log('Opening new websocket connection'); + console.log("[Services:addWebsocketConnection] 🕸️ Ouverture d'une nouvelle connexion websocket..."); await initWebsocket(url); } @@ -136,7 +142,7 @@ export default class Services { * @param spAddress - The SP Address (value). */ public updateRelay(url: string, spAddress: string) { - console.log(`✅ Updating relay ${url} with spAddress ${spAddress}`); + console.log(`[Services:updateRelay] ✅ Mise à jour du relais ${url} avec spAddress ${spAddress}`); this.relayAddresses[url] = spAddress; } @@ -146,7 +152,7 @@ export default class Services { * @returns The SP Address if found, or undefined if not. */ public getSpAddress(wsurl: string): string | undefined { - return this.relayAddresses[wsurl]; + return this.relayAddresses[wsurl]; } /** @@ -154,51 +160,40 @@ export default class Services { * @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, - })); + return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({ + wsurl, + spAddress, + })); } /** * Print all key/value pairs for debugging. */ public printAllRelays(): void { - console.log("Current relay addresses:"); - for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) { - console.log(`${wsurl} -> ${spAddress}`); - } + 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(); - console.log('isPaired() called, result:', result); - - // Additional debugging: check device state - try { - const device = this.dumpDeviceFromMemory(); - console.log('Current device state:', { - pairing_process_commitment: device.pairing_process_commitment, - paired_member: device.paired_member - }); - } catch (deviceError) { - console.error('Failed to dump device for debugging:', deviceError); - } - return result; } catch (e) { - throw new Error(`isPaired ~ Error: ${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(`Failed to unpair device: ${e}`); + throw new Error(`[Services:unpairDevice] Échec de la dissociation: ${e}`); } } @@ -225,6 +220,25 @@ export default class Services { 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'; @@ -233,106 +247,211 @@ export default class Services { } private async getTokensFromFaucet(): Promise { + console.log('[Services:getTokensFromFaucet] 🚰 Demande de tokens au faucet...'); try { await this.ensureSufficientAmount(); } catch (e) { - console.error('Failed to get tokens from relay, check connection'); + console.error('[Services:getTokensFromFaucet] ❌ Échec, vérifiez la connexion au relais.'); return; } } - // If we're updating a process, we must call that after update especially if roles are part of it - // We will take the roles from the last state, wheter it's commited or not - public async checkConnections(process: Process, stateId: string | null = null): Promise { - if (process.states.length < 2) { - throw new Error("Process doesn't have any state yet"); + /** + * Tente d'établir des connexions (secrets partagés) avec les membres d'un état de processus. + */ + public async ensureConnections(process: Process, stateId: string | null = null): Promise { + const processId = process?.process_id; // Utilisation de l'optional chaining au cas où process est null + console.info(`[ConnectionCheck] 🔄 Démarrage de la vérification des connexions pour le processus ${processId} (StateID: ${stateId || 'par défaut'})`); + + if (!process) { + console.error(`[ConnectionCheck] 💥 ERREUR CRITIQUE: ensureConnections a été appelée avec un processus nul ou undefined.`); + return; } - let roles: Record | null = null; - if (!stateId) { - roles = process.states[process.states.length - 2].roles; - } else { - roles = process.states.find(state => state.state_id === stateId)?.roles || null; - } - if (!roles) { - throw new Error('No roles found'); - } - let members: Set = new Set(); - for (const role of Object.values(roles!)) { - for (const member of role.members) { - // Check if we know the member that matches this id - const memberAddresses = this.getAddressesForMemberId(member); - if (memberAddresses && memberAddresses.length != 0) { - members.add({ sp_addresses: memberAddresses }); - } - } + + // 1. Déterminer quel état analyser + const state = this.getStateToCheck(process, stateId); + if (!state) { + console.warn(`[ConnectionCheck] ⚠️ Aucun état valide trouvé pour le processus ${processId}. (States: ${process.states.length}, StateID demandé: ${stateId}). Abandon.`); + return; } + + // 2. Tenter de trouver les membres dans les rôles de cet état + // --- AMÉLIORATION: Appel 'await' ajouté pour corriger la race condition --- + let members = await this.getMembersFromState(state); if (members.size === 0) { - // This must be a pairing process - // Check if we have a pairedAddresses in the public data - let publicData: Record | null = null; - if (!stateId) { - publicData = process.states[process.states.length - 2]?.public_data; - } else { - publicData = process.states.find(state => state.state_id === stateId)?.public_data || null; - } - - // If pairedAddresses is not in the current state, look in previous states - if (!publicData?.['pairedAddresses']) { - // Look for pairedAddresses in previous states - for (let i = process.states.length - 1; i >= 0; i--) { - const state = process.states[i]; - if (state.public_data && state.public_data['pairedAddresses']) { - publicData = state.public_data; - break; - } - } - } - - if (!publicData?.['pairedAddresses']) { - throw new Error('Not a pairing process'); - } - const decodedAddresses = this.decodeValue(publicData['pairedAddresses']); - if (decodedAddresses.length === 0) { - throw new Error('Not a pairing process'); - } - members.add({ sp_addresses: decodedAddresses }); + 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 } - // Ensure the amount is available before proceeding - await this.getTokensFromFaucet(); - const unconnectedAddresses = new Set(); - const myAddress = await this.getDeviceAddress(); - for (const member of Array.from(members)) { - const sp_addresses = member.sp_addresses; - if (!sp_addresses || sp_addresses.length === 0) { - continue; - } - for (const address of sp_addresses) { - // For now, we ignore our own device address, although there might be use cases for having a secret with ourselves - if (address === myAddress) { - continue; - } - if ((await this.getSecretForAddress(address)) === null) { - unconnectedAddresses.add(address); - } - } + if (members.size === 0) { + console.log(`[ConnectionCheck] 🏁 Aucun membre (rôles ou pairing) trouvé à qui se connecter. Tâche terminée.`); + return; } - if (unconnectedAddresses && unconnectedAddresses.size != 0) { + + // 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)); - await this.handleApiReturn(apiResult); + + 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); } } - public async connectAddresses(addresses: string[]): Promise { - if (addresses.length === 0) { - throw new Error('Trying to connect to empty addresses list'); + // --- 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 { - return this.sdkClient.create_transaction(addresses, 1); - } catch (e) { - console.error('Failed to connect member:', e); - throw e; + // 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 + } } } @@ -341,10 +460,12 @@ export default class Services { 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.`); } } @@ -356,19 +477,20 @@ export default class Services { 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('Amount is still 0 after 3 attempts'); + 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('Device already paired'); + throw new Error("L'appareil est déjà appairé"); } const myAddress: string = this.sdkClient.get_address(); pairWith.push(myAddress); @@ -391,27 +513,18 @@ export default class Services { min_sig_member: 1.0, }, ], - storages: [STORAGEURL] + storages: [STORAGEURL], }, }; try { - return this.createProcess( - privateData, - publicData, - roles - ); + return this.createProcess(privateData, publicData, roles); } catch (e) { - throw new Error(`Creating process failed:, ${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 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) { @@ -429,152 +542,206 @@ export default class Services { return { jsonCompatibleData, binaryData }; } - public async createProcess( - privateData: Record, - publicData: Record, - roles: Record, - ): Promise { - let relayAddress = this.getAllRelays()[0]?.spAddress; + // --- 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('⏳ Waiting for relays to be ready...'); + 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('❌ No relay address available after waiting'); + throw new Error('[Services:getAvailableRelayAddress] ❌ Aucune adresse de relais disponible après attente'); } - - const feeRate = 1; + return relayAddress; + } - // We can't encode files as the rest because Uint8Array is not valid json - // So we first take them apart and we will encode them separately and put them back in the right object - // TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking + /** + * 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 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) + const encodedPublicData = { + ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), + ...this.sdkClient.encode_binary(publicSplitData.binaryData), }; - // console.log('encodedPrivateData:', encodedPrivateData); - // console.log('encodedPublicData:', encodedPublicData); - // console.log('roles:', roles); - // console.log('members:', this.getAllMembers()); - // console.log('relayAddress:', relayAddress, 'feeRate:', feeRate); - - await this.getTokensFromFaucet(); - const result = this.sdkClient.create_new_process ( - encodedPrivateData, - roles, - encodedPublicData, - relayAddress, - feeRate, - this.getAllMembers() - ); - - if (result.updated_process) { - console.log('created process:', result.updated_process); - await this.checkConnections(result.updated_process.current_process); - return(result); - } else { - throw new Error('Empty updated_process in createProcessReturn'); - } + return { encodedPrivateData, encodedPublicData }; } public async updateProcess(process: Process, privateData: Record, publicData: Record, roles: Record | null): Promise { + console.log(`[Services:updateProcess] 🔄 Mise à jour du processus ${process.process_id}...`); // If roles is null, we just take the last commited state roles if (!roles) { roles = this.getRoles(process); } else { - // We should check that we have the right to change the roles here, or maybe it's better leave it to the wasm - console.log('Provided new roles:', JSON.stringify(roles)); + console.log('[Services:updateProcess] ℹ️ Utilisation de nouveaux rôles fournis:', JSON.stringify(roles)); } const privateSplitData = this.splitData(privateData); const publicSplitData = this.splitData(publicData); - const encodedPrivateData = { - ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), - ...this.sdkClient.encode_binary(privateSplitData.binaryData) + const 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) + const encodedPublicData = { + ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), + ...this.sdkClient.encode_binary(publicSplitData.binaryData), }; try { const result = this.sdkClient.update_process(process, encodedPrivateData, roles, encodedPublicData, this.getAllMembers()); if (result.updated_process) { - await this.checkConnections(result.updated_process.current_process); - return(result); + console.log(`[Services:updateProcess] ✅ Processus ${process.process_id} mis à jour. Vérification des connexions...`); + await this.ensureConnections(result.updated_process.current_process); + return result; } else { - throw new Error('Empty updated_process in updateProcessReturn'); + throw new Error('[Services:updateProcess] 💥 updated_process vide dans updateProcessReturn'); } } catch (e) { - throw new Error(`Failed to update process: ${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('Unknown process'); + throw new Error('[Services:createPrdUpdate] 💥 Processus inconnu'); } else { - await this.checkConnections(process); + await this.ensureConnections(process); } try { return this.sdkClient.create_update_message(process, stateId, this.getAllMembers()); } catch (e) { - throw new Error(`Failed to create prd update: ${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('Unknown process'); + throw new Error('[Services:createPrdResponse] 💥 Processus inconnu'); } try { return this.sdkClient.create_response_prd(process, stateId, this.getAllMembers()); } catch (e) { - throw new Error(`Failed to create response prd: ${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('Failed to get process from db'); + 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.checkConnections(result.updated_process.current_process); - return(result); + await this.ensureConnections(result.updated_process.current_process); + return result; } else { - throw new Error('Empty updated_process in approveChangeReturn'); + throw new Error('[Services:approveChange] 💥 updated_process vide dans approveChangeReturn'); } } catch (e) { - throw new Error(`Failed to create prd response: ${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('Failed to get process from db'); + 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(`Failed to create prd response: ${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 @@ -584,17 +751,21 @@ export default class Services { 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); @@ -602,64 +773,100 @@ export default class Services { } 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.log('parsing new cipher'); + // 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); - - // Device 1 wait Device 2 - const waitingModal = document.getElementById('waiting-modal'); - if (waitingModal) { - this.device2Ready = true; - } - } catch (e) { - console.error(`Parsed cipher with error: ${e}`); + console.error(`[Services:parseCipher] 💥 Échec critique du déchiffrement: ${e}`); + console.warn(`[Services:parseCipher] Impossible de déchiffrer un message entrant.`); + console.warn(`[Services:parseCipher] Contrainte d'anonymat: L'expéditeur est inconnu, impossible d'invalider un seul secret.`); + console.warn(`[Services:parseCipher] ACTION: Réinitialisation de TOUS les secrets pour forcer une reconnexion...`); + + // Appel de la "bombe" pour casser la boucle de secrets périmés + await this.clearSecretsFromDB(); + + console.warn(`[Services:parseCipher] ACTION: Forçage d'une reconnexion à tous les processus connus...`); + try { + // On récupère tous les processus qu'on connaît + const allProcesses = await this.getProcesses(); + + for (const process of Object.values(allProcesses)) { + // On force la vérification des connexions. + // Comme les secrets sont vides, 'ensureConnections' va (via 'findUnconnectedAddresses') + // appeler 'connectAddresses' et recréer les secrets. + await this.ensureConnections(process); + } + console.log('[Services:parseCipher] ✅ Resynchronisation des secrets terminée.'); + } catch (reconnectError) { + console.error('[Services:parseCipher] 💥 Échec lors de la tentative de reconnexion post-nettoyage:', reconnectError); + } } - // await this.saveCipherTxToDb(parsedTx) } + // --- AMÉLIORATION: Refactorisé en sous-fonctions --- async parseNewTx(newTxMsg: string) { - const parsedMsg: NewTxMessage = JSON.parse(newTxMsg); + console.log('[Services:parseNewTx] 📄 Nouveau message NewTx reçu.'); + const parsedMsg: NewTxMessage = JSON.parse(newTxMsg); if (parsedMsg.error !== null) { - console.error('Received error in new tx message:', parsedMsg.error); + 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 { - // Does the transaction spend the tip of a process? - const prevouts = this.sdkClient.get_prevouts(parsedMsg.transaction); - console.log('prevouts:', prevouts); + 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(parsedMsg.transaction); - console.log('Transaction', newTip, 'spends the tip of process', processId); - // We take the data out of the output - const newStateId = this.sdkClient.get_opreturn(parsedMsg.transaction); - console.log('newStateId:', newStateId); - // We update the relevant process + const processId = process.process_id; // Utilisation de l'ID stocké + const newTip = this.sdkClient.get_txid(transaction); + console.log(`[Services:updateProcessesFromNewTx] 🔗 La Tx ${newTip} dépense le tip du processus ${processId}`); + + const newStateId = this.sdkClient.get_opreturn(transaction); + console.log('[Services:updateProcessesFromNewTx] 📄 Nouvel stateId (op_return):', newStateId); + const updatedProcess = this.sdkClient.process_commit_new_state(process, newStateId, newTip); this.processesCache[processId] = updatedProcess; - console.log('updatedProcess:', updatedProcess); - break; + 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('Failed to parse new tx for commitments:', 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(); @@ -671,57 +878,127 @@ export default class Services { } await this.saveDeviceInDatabase(newDevice); + console.log('[Services:updateWalletFromNewTx] ✅ Appareil mis à jour et sauvegardé.'); } catch (e) { - console.error('Failed to update device with new tx'); + 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) { - console.debug(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(apiReturn); + console.log("[Services:handleApiReturn] 📥 Traitement d'un nouvel objet ApiReturn...", apiReturn); - // Skip processing if apiReturn is empty or contains only null values + // 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) { - console.log('Skipping empty apiReturn'); - return; + return false; } + const hasValidData = Object.values(apiReturn).some((value) => value !== null && value !== undefined); + return hasValidData; + } - // Check if all values are null - const hasValidData = Object.values(apiReturn).some(value => value !== null && value !== undefined); - if (!hasValidData) { - console.log('Skipping apiReturn with only null values'); - return; + 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; } - if (apiReturn.partial_tx) { - try { - const res = this.sdkClient.sign_transaction(apiReturn.partial_tx); - apiReturn.new_tx_to_send = res.new_tx_to_send; - } catch (e) { - console.error('Failed to sign transaction:', e); + } + + 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); + } } } - if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { - this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); - await new Promise(r => setTimeout(r, 500)); - } - - if (apiReturn.secrets) { - const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets; - const confirmedSecrets = apiReturn.secrets.shared_secrets; - - const db = await Database.getInstance(); - for (const secret of unconfirmedSecrets) { - await db.addObject({ - storeName: 'unconfirmed_secrets', - object: secret, - key: null, - }); - } - const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value })); + // 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({ @@ -729,90 +1006,108 @@ export default class Services { object: entry.value, key: entry.key, }); + console.log(`[Services:handleSecrets] ✅ Secret partagé pour ${entry.key} sauvegardé.`); } catch (e) { - throw e; - } - - // We don't want to throw an error, it could simply be that we registered directly the shared secret - // this.removeUnconfirmedSecret(entry.value); - } - } - - if (apiReturn.updated_process) { - const updatedProcess = apiReturn.updated_process; - - const processId: string = updatedProcess.process_id; - - if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) { - for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) { - const blob = this.hexToBlob(cipher); - try { - await this.saveBlobToDb(hash, blob); - } catch (e) { - console.error(e); - } + console.error(`[Services:handleSecrets] 💥 Échec de l'ajout du secret partagé pour ${entry.key}:`, e); } } - - // Save process to db - await this.saveProcessToDb(processId, updatedProcess.current_process); - - if (updatedProcess.diffs && updatedProcess.diffs.length != 0) { - try { - await this.saveDiffsToDb(updatedProcess.diffs); - } catch (e) { - console.error('Failed to save diffs to db:', e); - } - } - - // Check if this is a pairing process that's ready for confirmation - const existingDevice = await this.getDeviceFromDatabase(); - if (existingDevice && existingDevice.pairing_process_commitment === processId) { - // This is our pairing process, check if it has paired addresses - const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1]; - if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) { - console.log('Pairing process updated with paired addresses, confirming pairing...'); - try { - await this.confirmPairing(); - } catch (e) { - console.error('Failed to auto-confirm pairing:', e); - } - } - } - } - - if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) { - for (const hash of apiReturn.push_to_storage) { - const blob = await this.getBlobFromDb(hash); - if (blob) { - // Get the storages from the diff data - const diff = await this.getDiffByValueFromDb(hash); - if (diff) { - const storages = diff.storages; - await this.saveDataToStorage(hash, storages, blob, null); - } else { - console.error('Failed to get diff from db for hash', hash); - } - } else { - console.error('Failed to get data from db for hash', hash); - } - } - } - - if (apiReturn.commit_to_send) { - const commit = apiReturn.commit_to_send; - this.sendCommitMessage(JSON.stringify(commit)); - } - - if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) { - this.sendCipherMessages(apiReturn.ciphers_to_send); } } + private async handleUpdatedProcess(updatedProcess: any) { + const processId: string = updatedProcess.process_id; + console.log(`[Services:handleUpdatedProcess] 🔄 Traitement des mises à jour pour le processus ${processId}`); + + // Sauvegarder les données chiffrées + if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) { + await this.saveEncryptedData(updatedProcess.encrypted_data); + } + + // Sauvegarder le processus lui-même + await this.saveProcessToDb(processId, updatedProcess.current_process); + + // Sauvegarder les diffs + if (updatedProcess.diffs && updatedProcess.diffs.length != 0) { + try { + await this.saveDiffsToDb(updatedProcess.diffs); + } catch (e) { + console.error('[Services:handleUpdatedProcess] 💥 Échec de la sauvegarde des diffs:', e); + } + } + + // Vérifier la logique métier spécifique au pairing + await this.checkAndConfirmPairing(processId, updatedProcess); + } + + private async saveEncryptedData(encryptedData: Record) { + console.log(`[Services:saveEncryptedData] 💾 Sauvegarde de ${Object.keys(encryptedData).length} blob(s) chiffré(s)...`); + for (const [hash, cipher] of Object.entries(encryptedData)) { + const blob = this.hexToBlob(cipher); + try { + await this.saveBlobToDb(hash, blob); + } catch (e) { + console.error(`[Services:saveEncryptedData] 💥 Échec de la sauvegarde du blob pour ${hash}:`, e); + } + } + } + + private async checkAndConfirmPairing(processId: string, updatedProcess: any) { + try { + const existingDevice = await this.getDeviceFromDatabase(); + if (!existingDevice || existingDevice.pairing_process_commitment !== processId) { + // console.debug('[Services:checkAndConfirmPairing] ℹ️ Ce n\'est pas le processus de pairing de cet appareil. Skip.'); + return; // Ce n'est pas le processus de pairing de cet appareil + } + + // C'est notre processus de pairing, vérifions s'il est prêt + const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1]; + if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) { + console.log('[Services:checkAndConfirmPairing] 🤝 Processus de pairing mis à jour avec les adresses. Confirmation automatique...'); + await this.confirmPairing(); + } + } catch (e) { + console.error("[Services:checkAndConfirmPairing] 💥 Échec de l'auto-confirmation du pairing:", e); + } + } + + private async handlePushToStorage(hashes: string[]) { + console.log(`[Services:handlePushToStorage] ☁️ Demande de push pour ${hashes.length} hash(es)`); + for (const hash of hashes) { + try { + const blob = await this.getBlobFromDb(hash); + if (!blob) { + console.error(`[Services:handlePushToStorage] 💥 Échec: blob non trouvé en BDD pour le hash ${hash}`); + continue; + } + + const diff = await this.getDiffByValueFromDb(hash); + if (!diff) { + console.error(`[Services:handlePushToStorage] 💥 Échec: diff non trouvé en BDD pour le hash ${hash}`); + continue; + } + + const storages = diff.storages; + console.log(`[Services:handlePushToStorage] ☁️ Poussée de ${hash} vers ${storages.length} storage(s)...`); + await this.saveDataToStorage(storages, hash, blob, null); + } catch (e) { + console.error(`[Services:handlePushToStorage] 💥 Échec du push pour ${hash}:`, e); + } + } + } + + private handleCommit(commit: any) { + this.sendCommitMessage(JSON.stringify(commit)); + } + + private handleCiphers(ciphers: any[]) { + this.sendCipherMessages(ciphers); + } + public async openPairingConfirmationModal(processId: string) { + console.log('[Services:openPairingConfirmationModal] 띄 Ouverture du modal de confirmation...'); const process = await this.getProcess(processId); if (!process) { - console.error('Failed to find pairing process'); + console.error('[Services:openPairingConfirmationModal] 💥 Échec: processus de pairing non trouvé'); return; } const firstState = process.states[0]; @@ -826,20 +1121,22 @@ export default class Services { } 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('No pairing process commitment found'); + 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('Unknown pairing process'); + console.error('[Services:confirmPairing] 💥 Processus de pairing inconnu'); return; } @@ -848,95 +1145,84 @@ export default class Services { if (!myPairingState && myPairingProcess.states.length > 0) { // If no committed state, use the current state myPairingState = myPairingProcess.states[myPairingProcess.states.length - 1]; - console.log('Using current state instead of committed state'); + console.log('[Services:confirmPairing] ⚠️ Utilisation de l\'état actuel au lieu de l\'état "commited"'); } if (!myPairingState) { - console.error('No state found in pairing process'); + console.error('[Services:confirmPairing] 💥 Aucun état trouvé dans le processus de pairing'); return; } const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; if (!encodedSpAddressList) { - console.error('No paired addresses found in state'); + 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('Empty pairedAddresses'); + console.error('[Services:confirmPairing] 💥 pairedAddresses est vide'); return; } - - // Test parsing côté Rust - console.log('Checking if test_process_id_parsing is available:', typeof this.sdkClient.test_process_id_parsing); - try { - if (this.sdkClient.test_process_id_parsing) { - const rustParseResult = this.sdkClient.test_process_id_parsing(pairingProcessId); - console.log('Rust parsing test result:', rustParseResult); - } else { - console.error('test_process_id_parsing function not found in sdkClient'); - console.log('Available functions:', Object.keys(this.sdkClient).filter(key => typeof this.sdkClient[key] === 'function')); - } - } catch (rustParseError) { - console.error('Rust parsing test failed:', rustParseError); - } - + 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('pair_device() call succeeded'); + console.log('[Services:confirmPairing] ✅ Appel de pair_device() réussi (côté SDK).'); } catch (pairError) { - console.error('pair_device() failed:', 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('Is paired after pair_device call:', isPairedAfterPairing); + console.log('[Services:confirmPairing] ❓ Statut is_paired après appel:', isPairedAfterPairing); // Save the updated device const newDevice = this.dumpDeviceFromMemory(); - console.log('Device from memory after pairing:', { + console.log('[Services:confirmPairing] ℹ️ Appareil en mémoire après appairage:', { pairing_process_commitment: newDevice.pairing_process_commitment, - paired_member: newDevice.paired_member + paired_member: newDevice.paired_member, }); - + // IMPORTANT: Only set pairing_process_commitment if WASM pairing succeeded if (isPairedAfterPairing) { - console.log('WASM pairing succeeded, keeping WASM commitment'); - // Don't override - use what WASM set + console.log("[Services:confirmPairing] ℹ️ L'appairage WASM a réussi, conservation de l'engagement WASM"); } else { - console.log('WASM pairing failed, manually setting commitment'); + 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('Final is_paired status:', finalIsPaired); - console.log('Device successfully paired with process:', pairingProcessId); - + 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('Failed to confirm pairing:', 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('Failed to get pairing process id'); + console.error("[Services:updateDevice] 💥 Échec de l'obtention du pairing process id"); return; } const myPairingProcess = await this.getProcess(myPairingProcessId); if (!myPairingProcess) { - console.error('Unknown pairing process'); + console.error('[Services:updateDevice] 💥 Processus de pairing inconnu'); return; } const myPairingState = this.getLastCommitedState(myPairingProcess); @@ -944,19 +1230,22 @@ export default class Services { const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; const spAddressList = this.decodeValue(encodedSpAddressList); if (spAddressList.length === 0) { - console.error('Empty pairedAddresses'); + 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.'); } } @@ -964,7 +1253,7 @@ export default class Services { try { this.sdkClient.pair_device(processId, spAddressList); } catch (e) { - throw new Error(`Failed to pair device: ${e}`); + throw new Error(`[Services:pairDevice] 💥 Échec: ${e}`); } } @@ -977,7 +1266,7 @@ export default class Services { try { return this.sdkClient.get_address(); } catch (e) { - throw new Error(`Failed to get device address: ${e}`); + throw new Error(`[Services:getDeviceAddress] 💥 Échec: ${e}`); } } @@ -985,7 +1274,7 @@ export default class Services { try { return this.sdkClient.dump_device(); } catch (e) { - throw new Error(`Failed to dump device: ${e}`); + throw new Error(`[Services:dumpDeviceFromMemory] 💥 Échec: ${e}`); } } @@ -993,7 +1282,7 @@ export default class Services { try { return this.sdkClient.dump_neutered_device(); } catch (e) { - console.error(`Failed to dump device: ${e}`); + console.error(`[Services:dumpNeuteredDevice] 💥 Échec: ${e}`); return null; } } @@ -1002,7 +1291,7 @@ export default class Services { try { return this.sdkClient.get_pairing_process_id(); } catch (e) { - throw new Error(`Failed to get pairing process: ${e}`); + throw new Error(`[Services:getPairingProcessId] 💥 Échec (Probablement non appairé): ${e}`); } } @@ -1010,34 +1299,33 @@ export default class Services { const db = await Database.getInstance(); const walletStore = 'wallet'; try { - console.log('Saving device to database:', { + console.log("[Services:saveDeviceInDatabase] 💾 Sauvegarde de l'appareil en BDD...", { pairing_process_commitment: device.pairing_process_commitment, - paired_member: device.paired_member + paired_member: device.paired_member, }); - + const prevDevice = await this.getDeviceFromDatabase(); if (prevDevice) { - console.log('Previous device found, deleting...'); - await db.deleteObject(walletStore, "1"); + // 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('Device saved successfully'); - - // Verify save - const savedDevice = await this.getDeviceFromDatabase(); - console.log('Verification - saved device:', { - pairing_process_commitment: savedDevice?.pairing_process_commitment, - paired_member: savedDevice?.paired_member - }); - + + 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('Error saving device to database:', e); + console.error('[Services:saveDeviceInDatabase] 💥 Erreur lors de la sauvegarde:', e); } } @@ -1052,7 +1340,7 @@ export default class Services { return null; } } catch (e) { - throw new Error(`Failed to retrieve device from db: ${e}`); + throw new Error(`[Services:getDeviceFromDatabase] 💥 Échec: ${e}`); } } @@ -1060,13 +1348,13 @@ export default class Services { try { const device = await this.getDeviceFromDatabase(); if (device) { - const pairedMember = device['paired_member']; + const pairedMember = device['paired_member']; return pairedMember.sp_addresses; } else { return null; } } catch (e) { - throw new Error(`Failed to retrieve paired_member from device: ${e}`); + throw new Error(`[Services:getMemberFromDevice] 💥 Échec: ${e}`); } } @@ -1086,7 +1374,8 @@ export default class Services { try { us = this.sdkClient.get_pairing_process_id(); } catch (e) { - throw e; + // Si non appairé, nous ne pouvons être dans aucun rôle + return false; } return this.rolesContainsMember(roles, us); @@ -1098,7 +1387,6 @@ export default class Services { return true; } } - return false; } @@ -1115,12 +1403,14 @@ export default class Services { 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 ~ Error:', e); + console.error('[Services:createNewDevice] 💥 Erreur:', e); } return spAddress; @@ -1128,6 +1418,7 @@ export default class Services { 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); @@ -1136,26 +1427,30 @@ export default class Services { public async updateDeviceBlockHeight(): Promise { if (this.currentBlockHeight === -1) { - throw new Error('Current block height not set'); + 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(`Failed to get device from database: ${e}`); + throw new Error(`[Services:updateDeviceBlockHeight] 💥 Échec de l'obtention de l'appareil depuis la BDD: ${e}`); } if (!device) { - throw new Error('Device not found'); + console.error('[Services:updateDeviceBlockHeight] 💥 Appareil non trouvé. Skip.'); + return; } const birthday = device.sp_wallet.birthday; if (birthday === undefined || birthday === null) { - throw new Error('Birthday not found'); + 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 @@ -1166,16 +1461,16 @@ export default class Services { // Then save it to database await this.saveDeviceInDatabase(device); } catch (e) { - throw new Error(`Failed to save updated device: ${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) { - // We need to catch up + 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(`Failed to scan blocks: ${e}`); + console.error(`[Services:updateDeviceBlockHeight] 💥 Échec du scan des blocs: ${e}`); return; } @@ -1183,11 +1478,12 @@ export default class Services { try { const device = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(device); + console.log('[Services:updateDeviceBlockHeight] ✅ Scan terminé et appareil sauvegardé.'); } catch (e) { - console.error(`Failed to save updated device: ${e}`); + console.error(`[Services:updateDeviceBlockHeight] 💥 Échec de la sauvegarde de l'appareil après scan: ${e}`); } } else { - // Up to date, just returns + // console.debug('[Services:updateDeviceBlockHeight] ℹ️ Portefeuille à jour. Rien à faire.'); return; } } @@ -1198,6 +1494,7 @@ export default class Services { const storeName = 'processes'; try { + console.log(`[Services:removeProcess] 🗑️ Suppression du processus ${processId}`); await db.deleteObject(storeName, processId); } catch (e) { console.error(e); @@ -1208,13 +1505,14 @@ export default class Services { 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; } } @@ -1232,7 +1530,7 @@ export default class Services { // Update the process in the cache this.processesCache[processId] = process; } catch (e) { - console.error(`Failed to save process ${processId}: ${e}`); + console.error(`[Services:saveProcessToDb] 💥 Échec de la sauvegarde du processus ${processId}: ${e}`); } } @@ -1245,9 +1543,9 @@ export default class Services { key: hash, }); } catch (e) { - console.error(`Failed to save data to db: ${e}`); + console.error(`[Services:saveBlobToDb] 💥 Échec de la sauvegarde du blob ${hash}: ${e}`); } - } + } public async getBlobFromDb(hash: string): Promise { const db = await Database.getInstance(); @@ -1258,11 +1556,11 @@ export default class Services { } } - public async saveDataToStorage(hash: string, storages: string[], data: Blob, ttl: number | null) { + public async saveDataToStorage(storages: string[], hash: string, data: Blob, ttl: number | null) { try { await storeData(storages, hash, data, ttl); } catch (e) { - console.error(`Failed to store data with hash ${hash}: ${e}`); + console.error(`[Services:saveDataToStorage] 💥 Échec du stockage du hash ${hash}: ${e}`); } } @@ -1289,35 +1587,51 @@ export default class Services { }); } } catch (e) { - throw new Error(`Failed to save process: ${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]; - } else { + } + + // 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; - } else { - try { - const db = await Database.getInstance(); - this.processesCache = await db.dumpStore('processes'); - return this.processesCache; - } catch (e) { - throw e; - } + } + + // 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 { @@ -1335,27 +1649,31 @@ export default class Services { try { const processes: Record = await db.dumpStore('processes'); if (processes && Object.keys(processes).length != 0) { - console.log(`Restoring ${Object.keys(processes).length} processes`); + console.log(`[Services:restoreProcessesFromDB] 🔄 Restauration de ${Object.keys(processes).length} processus depuis la BDD vers le cache...`); this.processesCache = processes; } else { - console.log('No processes to restore!'); + console.log('[Services:restoreProcessesFromDB] ℹ️ Aucun processus à restaurer.'); } } catch (e) { throw e; } } + // --- AMÉLIORATION: Ajout de logs clairs --- public async clearSecretsFromDB() { + console.warn('[Services:clearSecretsFromDB] 💣 Réinitialisation de TOUS les secrets (partagés et non confirmés)...'); const db = await Database.getInstance(); try { await db.clearStore('shared_secrets'); await db.clearStore('unconfirmed_secrets'); + console.warn('[Services:clearSecretsFromDB] ✅ Magasins de secrets vidés.'); } catch (e) { - console.error(e); + console.error('[Services:clearSecretsFromDB] 💥 Échec de la suppression des secrets:', 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) { @@ -1374,11 +1692,12 @@ export default class Services { }); } - // Now we can transfer them to memory + // 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'); @@ -1388,6 +1707,7 @@ export default class Services { 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; } @@ -1397,15 +1717,16 @@ export default class Services { try { return this.sdkClient.decode_value(value); } catch (e) { - console.error(`Failed to decode value: ${e}`); + console.error(`[Services:decodeValue] 💥 Échec: ${e}`); return null; } } async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { + console.log(`[Services:decryptAttribute] 🔑 Tentative de déchiffrement de l'attribut '${attribute}' pour le processus ${processId}`); let hash = state.pcd_commitment[attribute]; if (!hash) { - // attribute doesn't exist + console.warn(`[Services:decryptAttribute] ⚠️ L'attribut '${attribute}' n'existe pas (pas de hash).`); return null; } let key = state.keys[attribute]; @@ -1413,9 +1734,10 @@ export default class Services { // If key is missing, request an update and then retry if (!key) { + console.warn(`[Services:decryptAttribute] ⚠️ Clé manquante pour '${attribute}'. Vérification de l'accès et demande aux pairs...`); const roles = state.roles; let hasAccess = false; - // If we're not supposed to have access to this attribute, ignore + // If we're not supposed to have access to this attribute, ignore for (const role of Object.values(roles)) { for (const rule of Object.values(role.validation_rules)) { if (rule.fields.includes(attribute)) { @@ -1428,26 +1750,43 @@ export default class Services { } } - if (!hasAccess) return null; + if (!hasAccess) { + console.log(`[Services:decryptAttribute] ⛔ Accès non autorisé à '${attribute}'. Abandon.`); + return null; + } - await this.checkConnections((await this.getProcess(processId))!); + const process = await this.getProcess(processId); + if (!process) { + console.error(`[Services:decryptAttribute] 💥 Impossible de trouver le processus ${processId} pour ensureConnections.`); + return null; + } + + await this.ensureConnections(process); // We should have the key, so we're going to ask other members for it + console.log(`[Services:decryptAttribute] 🗣️ Demande de données aux pairs pour '${attribute}'...`); await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); const maxRetries = 5; const retryDelay = 500; // delay in milliseconds let retries = 0; - + while ((!hash || !key) && retries < maxRetries) { - await new Promise(resolve => setTimeout(resolve, retryDelay)); + console.log(`[Services:decryptAttribute] ⏳ Attente de la clé... (Tentative ${retries + 1}/${maxRetries})`); + await new Promise((resolve) => setTimeout(resolve, retryDelay)); // Re-read hash and key after waiting - hash = state.pcd_commitment[attribute]; - key = state.keys[attribute]; + // AMÉLIORATION: On doit relire l'état complet depuis la BDD/cache, car 'state' est un instantané + const updatedProcess = await this.getProcess(processId); + const updatedState = this.getStateFromId(updatedProcess, state.state_id); + if (updatedState) { + hash = updatedState.pcd_commitment[attribute]; + key = updatedState.keys[attribute]; + } retries++; } } if (hash && key) { + console.log(`[Services:decryptAttribute] ℹ️ Clé et hash trouvés pour '${attribute}'. Tentative de déchiffrement...`); const blob = await this.getBlobFromDb(hash); if (blob) { // Decrypt the data @@ -1461,43 +1800,26 @@ export default class Services { if (clear) { // deserialize the result to get the actual data const decoded = this.sdkClient.decode_value(clear); + console.log(`[Services:decryptAttribute] ✅ Attribut '${attribute}' déchiffré avec succès.`); return decoded; } else { throw new Error('decrypt_data returned null'); } } catch (e) { - console.error(`Failed to decrypt data: ${e}`); + console.error(`[Services:decryptAttribute] 💥 Échec du déchiffrement (decrypt_data): ${e}`); } + } else { + console.error(`[Services:decryptAttribute] 💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`); } + } else { + console.error(`[Services:decryptAttribute] 💥 Échec: Clé ou hash manquant après ${maxRetries} tentatives pour '${attribute}'.`); } - + return null; } getNotifications(): any[] | null { - // return [ - // { - // id: 1, - // title: 'Notif 1', - // description: 'A normal notification', - // sendToNotificationPage: false, - // path: '/notif1', - // }, - // { - // id: 2, - // title: 'Notif 2', - // description: 'A normal notification', - // sendToNotificationPage: false, - // path: '/notif2', - // }, - // { - // id: 3, - // title: 'Notif 3', - // description: 'A normal notification', - // sendToNotificationPage: false, - // path: '/notif3', - // }, - // ]; + // ... (Logique inchangée) return this.notifications; } @@ -1506,13 +1828,14 @@ export default class Services { } 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 @@ -1521,13 +1844,15 @@ export default class Services { 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('No device loaded'); + console.error('[Services:createBackUp] 💥 Aucun appareil chargé'); return null; } @@ -1543,7 +1868,7 @@ export default class Services { secrets: secrets, processes: processes, }; - + console.log('[Services:createBackUp] ✅ Backup créé.'); return backUp; } @@ -1556,126 +1881,156 @@ export default class Services { this.device2Ready = false; } - - // Handle the handshake message + // --- AMÉLIORATION: Refactorisé en sous-fonctions --- public async handleHandshakeMsg(url: string, parsedMsg: any) { try { const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); - if (handshakeMsg.sp_address) { - this.updateRelay(url, handshakeMsg.sp_address); - this.relayAddresses[url] = handshakeMsg.sp_address; - this.resolveRelayReady(); - } - - console.log('handshakeMsg:', handshakeMsg); - this.currentBlockHeight = handshakeMsg.chain_tip; - console.log('this.currentBlockHeight:', this.currentBlockHeight); - this.updateDeviceBlockHeight(); - if (this.membersList && Object.keys(this.membersList).length === 0) { - // We start from an empty list, just copy it over - this.membersList = handshakeMsg.peers_list; - } else { - // We are incrementing our list - for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) { - this.membersList[processId] = member as Member; - } - } - setTimeout(async () => { - const newProcesses: OutPointProcessMap = handshakeMsg.processes_list; - if (!newProcesses || Object.keys(newProcesses).length === 0) { - console.debug('Received empty processes list from', url); - return; - } + // 1. Mettre à jour les infos du relais (SP Address, Block Height) + this.updateRelayInfo(url, handshakeMsg); - if (this.processesCache && Object.keys(this.processesCache).length === 0) { - // We restored db but cache is empty, meaning we're starting from scratch - try { - await this.batchSaveProcessesToDb(newProcesses); - } catch (e) { - console.error('Failed to save processes to db:', e); - } - } else { - // We need to update our processes with what relay provides - const toSave: Record = {}; - for (const [processId, process] of Object.entries(newProcesses)) { - const existing = await this.getProcess(processId); - if (existing) { - // Look for state id we don't know yet - let newStates: string[] = []; - let newRoles: Record[] = []; - for (const state of process.states) { - if (!state || !state.state_id) { continue; } // shouldn't happen - if (state.state_id === EMPTY32BYTES) { - // We check that the tip is the same we have, if not we update - const existingTip = existing.states[existing.states.length - 1].commited_in; - if (existingTip !== state.commited_in) { - console.log('Found new tip for process', processId); - existing.states.pop(); // We discard the last state - existing.states.push(state); - // We know that's the last state, so we just trigger the update - toSave[processId] = existing; - } - } else if (!this.lookForStateId(existing, state.state_id)) { - // We don't want to overwrite what we already have for existing processes - // We may end up overwriting the keys for example - // So the process we're going to save needs to merge new states with what we already have - const existingLastState = existing.states.pop(); - if (!existingLastState) { - // This should never happen - console.error('Failed to get last state for process', processId); - break; - } - existing.states.push(state); - existing.states.push(existingLastState); - toSave[processId] = existing; // We mark it for update - if (this.rolesContainsUs(state.roles)) { - newStates.push(state.state_id); - newRoles.push(state.roles); - } + // 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 { - // We already have the state, but we check if we have the keys - const existingState = this.getStateFromId(existing, state.state_id); - if (existingState!.keys && Object.keys(existingState!.keys).length != 0) { - // We have some keys, so we just assume everything ok and move on for now - continue; - } else { - // We verify we are part of the roles - const roles = state.roles; - if (this.rolesContainsUs(roles)) { - // We don't have keys, but we are part of the roles, so we need to request the keys - // that may also be because we are part of a role that don't have any fields - // It's possible but let's request for nothing anyway - newStates.push(state.state_id); - newRoles.push(roles); - } else { - // We are simply not involved, move on - continue; - } - } + continue; // Pas notre état } } - - if (newStates.length != 0) { - await this.checkConnections(existing); - await this.requestDataFromPeers(processId, newStates, newRoles); - } - // Otherwise we're probably just in the initial loading at page initialization - } else { - // We add it to db - toSave[processId] = process; } } - if (toSave && Object.keys(toSave).length > 0) { - console.log('batch saving processes to db', toSave); - await this.batchSaveProcessesToDb(toSave); + 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; } - }, 500) - } catch (e) { - console.error('Failed to parse init message:', e); + } + + 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 { @@ -1690,32 +2045,30 @@ export default class Services { /** * Waits for at least one handshake message to be received from any connected relay. - * This ensures that the relay addresses are fully populated and the member list is updated. - * @returns A promise that resolves when at least one handshake message is received. */ private async waitForHandshakeMessage(timeoutMs: number = 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.keys(this.relayAddresses).length > 0) { - console.log('Handshake message received (members or relays present)'); + 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(`No handshake message received after ${timeoutMs}ms timeout`)); + reject(new Error(`[Services:waitForHandshakeMessage] ❌ Aucun handshake reçu après ${timeoutMs}ms`)); return; } - + // Continue polling setTimeout(checkForHandshake, pollInterval); }; - + checkForHandshake(); }); } @@ -1725,9 +2078,7 @@ export default class Services { * @returns Un tableau contenant tous les membres */ public getAllMembersSorted(): Record { - return Object.fromEntries( - Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) - ); + return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))); } public getAllMembers(): Record { @@ -1736,17 +2087,35 @@ export default class Services { 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 } + 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)); + const res = memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item)); return res; } @@ -1755,23 +2124,26 @@ export default class Services { const content = JSON.parse(response); const error = content.error; const errorMsg = error['GenericError']; - const dontRetry = [ - 'State is identical to the previous state', - 'Not enough valid proofs', - 'Not enough members to validate', - ]; - if (dontRetry.includes(errorMsg)) { return; } + 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) + }, 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; @@ -1797,8 +2169,11 @@ export default class Services { 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 } + if (processName) { + return this.decodeValue(processName); + } else { + return null; + } } else { return null; } @@ -1810,7 +2185,7 @@ export default class Services { try { pairingProcessId = this.getPairingProcessId(); } catch (e) { - return null; + return null; // Pas appairé } if (!pairingProcessId) { return null; @@ -1823,7 +2198,6 @@ export default class Services { // MyProcesses automatically contains pairing process newMyProcesses.add(pairingProcessId); for (const [processId, process] of Object.entries(processes)) { - // We use myProcesses attribute to not reevaluate all processes everytime if (newMyProcesses.has(processId)) { continue; } @@ -1840,13 +2214,13 @@ export default class Services { this.myProcesses = newMyProcesses; // atomic update return Array.from(this.myProcesses); } catch (e) { - console.error("Failed to get processes:", e); + console.error('[Services:getMyProcesses] 💥 Échec:', e); return null; } } public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record[]) { - console.log('Requesting data from peers'); + 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); @@ -1859,12 +2233,12 @@ export default class Services { public hexToBlob(hexString: string): Blob { const uint8Array = this.hexToUInt8Array(hexString); - return new Blob([uint8Array], { type: "application/octet-stream" }); + 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"); + 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) { @@ -1878,7 +2252,7 @@ export default class Services { const buffer = await blob.arrayBuffer(); const bytes = new Uint8Array(buffer); return Array.from(bytes) - .map(byte => byte.toString(16).padStart(2, '0')) + .map((byte) => byte.toString(16).padStart(2, '0')) .join(''); } @@ -1894,14 +2268,14 @@ export default class Services { try { return this.sdkClient.validate_merkle_proof(proof, hash); } catch (e) { - throw new Error(`Failed to validate merkle proof: ${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); + const lastCommitedState = process.states.findLast((state) => state.commited_in !== processTip); if (lastCommitedState) { return lastCommitedState; } else { @@ -1923,13 +2297,13 @@ export default class Services { 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); + 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); + const state = process.states.find((state) => state.state_id === stateId); if (state) { return state; } else { @@ -1940,17 +2314,19 @@ export default class Services { 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); + 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; + return null; } public isPairingProcess(roles: Record): boolean { - if (Object.keys(roles).length != 1) { return false } + 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 @@ -1962,7 +2338,7 @@ export default class Services { public async updateMemberPublicName(process: Process, newName: string): Promise { const publicData = { - 'memberPublicName': newName + memberPublicName: newName, }; return await this.updateProcess(process, {}, publicData, null); diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index 15fea2d..87512d4 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -3,16 +3,31 @@ import axios, { AxiosResponse } from 'axios'; export async function storeData(servers: string[], key: string, value: Blob, ttl: number | null): Promise { for (const server of servers) { try { - // Handle relative paths (for development proxy) vs absolute URLs (for production) + // --- DÉBUT DE LA CORRECTION --- + // 1. On vérifie d'abord si la donnée existe en appelant le bon service + // On passe 'server' au lieu de 'url' pour que testData construise la bonne URL + const dataExists = await testData(server, key); + + if (dataExists) { + console.log('Data already stored:', key); + continue; + } else { + console.log('Data not stored for server, proceeding to POST:', key, server); + } + // --- FIN DE LA CORRECTION --- + + + // Construction de l'URL pour le POST (stockage) + // Cette partie était correcte let url: string; if (server.startsWith('/')) { - // Relative path - construct manually for proxy + // Relative path url = `${server}/store/${encodeURIComponent(key)}`; if (ttl !== null) { url += `?ttl=${ttl}`; } } else { - // Absolute URL - use URL constructor + // Absolute URL const urlObj = new URL(`${server}/store/${encodeURIComponent(key)}`); if (ttl !== null) { urlObj.searchParams.append('ttl', ttl.toString()); @@ -20,17 +35,11 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl url = urlObj.toString(); } - // Test first that data is not already stored - const testResponse = await testData(url, key); - if (testResponse) { - console.log('Data already stored:', key); - continue; - } else { - console.log('Data not stored for server:', key, server); - } + // La ligne ci-dessous a été supprimée car le test est fait au-dessus + // const testResponse = await testData(url, key); // <-- LIGNE BOGUÉE SUPPRIMÉE - // Send the encrypted ArrayBuffer as the raw request body. - const response = await axios.post(url, value, { + // Send the encrypted data as the raw request body. + const response = await axios.post(url, value, { // Note: c'est bien un POST sur 'url' headers: { 'Content-Type': 'application/octet-stream' }, @@ -43,7 +52,7 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl return response; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 409) { - return null; + return null; // 409 Conflict (Key already exists) } console.error('Error storing data:', error); } @@ -51,22 +60,21 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl return null; } +// Fonction retrieveData (inchangée, elle était correcte) export async function retrieveData(servers: string[], key: string): Promise { for (const server of servers) { try { - // Handle relative paths (for development proxy) vs absolute URLs (for production) const url = server.startsWith('/') - ? `${server}/retrieve/${key}` // Relative path - use as-is for proxy - : new URL(`${server}/retrieve/${key}`).toString(); // Absolute URL - construct properly + ? `${server}/retrieve/${key}` + : new URL(`${server}/retrieve/${key}`).toString(); console.log('Retrieving data', key,' from:', url); - // When fetching the data from the server: + const response = await axios.get(url, { responseType: 'arraybuffer' }); if (response.status === 200) { - // Validate that we received an ArrayBuffer if (response.data instanceof ArrayBuffer) { return response.data; } else { @@ -80,8 +88,9 @@ export async function retrieveData(servers: string[], key: string): Promise { +// --- FONCTION testData CORRIGÉE --- +// Elle prend 'server' au lieu de 'url' et construit sa propre URL '/test/...' +export async function testData(server: string, key: string): Promise { try { - const response = await axios.get(url); - if (response.status !== 200) { - console.error(`Test response status: ${response.status}`); - return false; - } + // Construit l'URL /test/... + const testUrl = server.startsWith('/') + ? `${server}/test/${encodeURIComponent(key)}` + : new URL(`${server}/test/${encodeURIComponent(key)}`).toString(); + + const response = await axios.get(testUrl); // Fait un GET sur /test/... + + // 200 OK = la donnée existe + return response.status === 200; - return true; } catch (error) { - console.error('Error testing data:', error); - return null; + if (axios.isAxiosError(error) && error.response?.status === 404) { + // 404 Not Found = la donnée n'existe pas. C'est une réponse valide. + return false; + } + // Toute autre erreur (serveur offline, 500, etc.) + console.error('Error testing data:', error); + return false; // On considère que le test a échoué } -} +} \ No newline at end of file diff --git a/src/utils/sp-address.utils.ts b/src/utils/sp-address.utils.ts index 3cb9db9..3b7dcbe 100755 --- a/src/utils/sp-address.utils.ts +++ b/src/utils/sp-address.utils.ts @@ -100,7 +100,7 @@ export async function displayEmojis(text: string) { // Verify Other address export function initAddressInput() { - const container = getCorrectDOM('login-4nk-component') as HTMLElement + const container = getCorrectDOM('login-4nk-component') as HTMLElement; const addressInput = container.querySelector('#addressInput') as HTMLInputElement; const emojiDisplay = container.querySelector('#emoji-display-2'); const okButton = container.querySelector('#okButton') as HTMLButtonElement; @@ -155,7 +155,7 @@ async function onCreateButtonClick() { // Don't call confirmPairing immediately - it will be called when the pairing process is complete console.log('Pairing process initiated. Waiting for completion...'); } catch (e) { - console.error(`onCreateButtonClick error: ${e}`); + console.error(`onCreateButtonClick error: ${e}`); } } @@ -164,17 +164,14 @@ export async function prepareAndSendPairingTx(): Promise { try { const relayAddress = service.getAllRelays(); - const createPairingProcessReturn = await service.createPairingProcess( - "", - [], - ); + const createPairingProcessReturn = await service.createPairingProcess('', []); if (!createPairingProcessReturn.updated_process) { throw new Error('createPairingProcess returned an empty new process'); } try { - await service.checkConnections(createPairingProcessReturn.updated_process.current_process, createPairingProcessReturn.updated_process.current_process.states[0].state_id); + await service.ensureConnections(createPairingProcessReturn.updated_process.current_process, createPairingProcessReturn.updated_process.current_process.states[0].state_id); } catch (e) { throw e; } @@ -194,7 +191,6 @@ export async function prepareAndSendPairingTx(): Promise { } await service.handleApiReturn(createPairingProcessReturn); - } catch (err) { console.error(err); } @@ -202,7 +198,7 @@ export async function prepareAndSendPairingTx(): Promise { export async function generateQRCode(spAddress: string) { try { - const container = getCorrectDOM('login-4nk-component') as HTMLElement + const container = getCorrectDOM('login-4nk-component') as HTMLElement; const currentUrl = 'https://' + window.location.host; const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress); const qrCode = container?.querySelector('.qr-code img'); @@ -213,9 +209,9 @@ export async function generateQRCode(spAddress: string) { } export async function generateCreateBtn() { - try{ - //Generate CreateBtn - const container = getCorrectDOM('login-4nk-component') as HTMLElement + try { + // Generate CreateBtn + const container = getCorrectDOM('login-4nk-component') as HTMLElement; const createBtn = container?.querySelector('.create-btn'); if (createBtn) { createBtn.textContent = 'CREATE'; @@ -223,5 +219,4 @@ export async function generateCreateBtn() { } catch (err) { console.error(err); } - } \ No newline at end of file