diff --git a/src/main.ts b/src/main.ts index 6161d68..c833ca7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,3 @@ -import Database from './services/database.service'; import Services from './services/service'; import { Router } from './router/index'; import './components/header/Header'; @@ -6,53 +5,58 @@ import './App'; import { IframeController } from './services/iframe-controller.service'; async function bootstrap() { - console.log("🚀 Démarrage de l'application 4NK..."); + console.log("🚀 Démarrage de l'application 4NK (Multi-Worker Architecture)..."); try { - // 1. Initialisation des Services (WASM, Sockets, Database...) + // 1. Initialisation des Services (Proxy vers Core & Network Workers) + // Cela va lancer les workers en arrière-plan const services = await Services.getInstance(); - // 2. Initialisation de la base de données (Web Worker + Service Worker) - await Database.getInstance(); + // ❌ SUPPRIMÉ : await Database.getInstance(); + // La BDD est maintenant gérée de manière autonome par le CoreWorker. - // Injection du Header dans le slot prévu dans index.html + // Injection du Header const headerSlot = document.getElementById('header-slot'); if (headerSlot) { headerSlot.innerHTML = ''; } - // Vérification basique de l'appareil (logique reprise de ton ancien router.ts) + // 2. Vérification / Création de l'appareil (via le Worker) const device = await services.getDeviceFromDatabase(); if (!device) { - console.log('✨ Nouvel appareil détecté, création en cours...'); + console.log('✨ Nouvel appareil détecté, création en cours via Worker...'); await services.createNewDevice(); } else { console.log("Restauration de l'appareil..."); - services.restoreDevice(device); + await services.restoreDevice(device); } - // Initialisation du contrôleur d'Iframe (API listeners) + // 3. Initialisation du contrôleur d'Iframe (Reste sur le Main Thread pour écouter window) await IframeController.init(); - // 3. Restauration des données + // 4. Restauration des données (Appels asynchrones au Worker) await services.restoreProcessesFromDB(); - await services.restoreSecretsFromDB(); - // 4. Connexion réseau + // ⚠️ Assurez-vous d'avoir ajouté 'restoreSecretsFromDB' dans votre CoreWorker & Services + if (services.restoreSecretsFromDB) { + await services.restoreSecretsFromDB(); + } else { + console.warn("restoreSecretsFromDB non implémenté dans le proxy Services"); + } + + // 5. Connexion réseau (Network Worker) await services.connectAllRelays(); - // 5. Démarrage du Routeur (Affichage de la page) + // 6. Gestion du Routing const isIframe = window.self !== window.top; + const isPaired = await services.isPaired(); // Appel async maintenant - // On redirige vers 'process' SEULEMENT si on est appairé ET qu'on n'est PAS dans une iframe - if (services.isPaired() && !isIframe) { + if (isPaired && !isIframe) { console.log('✅ Mode Standalone & Appairé : Redirection vers Process.'); window.history.replaceState({}, '', 'process'); Router.handleLocation(); } else { - // Cas 1 : Pas appairé - // Cas 2 : Mode Iframe (même si appairé, on reste sur Home pour attendre le parent) - console.log(isIframe ? '📡 Mode Iframe détecté : Démarrage sur Home pour attente API.' : '🆕 Non appairé : Démarrage sur Home.'); + console.log(isIframe ? '📡 Mode Iframe détecté : Attente API.' : '🆕 Non appairé : Démarrage sur Home.'); Router.init(); } } catch (error) { @@ -60,6 +64,4 @@ async function bootstrap() { } } -// Lancement -bootstrap(); - +bootstrap(); \ No newline at end of file diff --git a/src/services/core/network.service.ts b/src/services/core/network.service.ts index 4550461..e4a29bc 100644 --- a/src/services/core/network.service.ts +++ b/src/services/core/network.service.ts @@ -1,83 +1,88 @@ -import { initWebsocket, sendMessage } from '../websockets.service.ts'; -import { AnkFlag } from '../../../pkg/sdk_client'; +import * as Comlink from 'comlink'; +import type { NetworkBackend } from '../../workers/network.worker'; +import Services from '../service'; // Attention à la dépendance circulaire, on va gérer ça export class NetworkService { - private relayAddresses: { [wsurl: string]: string } = {}; - private relayReadyResolver: (() => void) | null = null; - private relayReadyPromise: Promise | null = null; + private worker: Comlink.Remote; + private workerInstance: Worker; - constructor(private bootstrapUrls: string[]) {} + // Cache local pour répondre instantanément aux demandes synchrones de l'UI + private localRelays: Record = {}; - public async connectAllRelays(): Promise { - const connectedUrls: string[] = []; - for (const wsurl of Object.keys(this.relayAddresses)) { - try { - await this.addWebsocketConnection(wsurl); - connectedUrls.push(wsurl); - } catch (error) { - console.error(`[Network] ❌ Échec connexion ${wsurl}:`, error); - } + constructor(private bootstrapUrls: string[]) { + this.workerInstance = new Worker( + new URL('../../workers/network.worker.ts', import.meta.url), + { type: 'module' } + ); + this.worker = Comlink.wrap(this.workerInstance); + } + + // Initialisation appelée par Services.ts + public async initRelays() { + // 1. Setup des callbacks : Quand le worker reçoit un message, il appelle ici + await this.worker.setCallbacks( + Comlink.proxy(this.onMessageReceived.bind(this)), + Comlink.proxy(this.onStatusChange.bind(this)) + ); + + // 2. Lancer les connexions + for (const url of this.bootstrapUrls) { + this.addWebsocketConnection(url); } } - public async addWebsocketConnection(url: string): Promise { - console.log(`[Network] 🔌 Connexion à: ${url}`); - await initWebsocket(url); + // --- MÉTHODES PUBLIQUES (API inchangée) --- + + public async addWebsocketConnection(url: string) { + await this.worker.connect(url); } - public initRelays() { - for (const wsurl of this.bootstrapUrls) { - this.updateRelay(wsurl, ''); + public async connectAllRelays() { + for (const url of this.bootstrapUrls) { + this.addWebsocketConnection(url); } } + public async sendMessage(flag: string, content: string) { + // On transmet au worker + // Note: Le type 'any' est utilisé pour simplifier la compatibilité avec AnkFlag + await this.worker.sendMessage(flag as any, content); + } + public updateRelay(url: string, spAddress: string) { - console.log(`[Network] Mise à jour relais ${url} -> ${spAddress}`); - this.relayAddresses[url] = spAddress; - if (spAddress) this.resolveRelayReady(); - } - - public getAvailableRelayAddress(): Promise { - let relayAddress = Object.values(this.relayAddresses).find((addr) => addr !== ''); - if (relayAddress) return Promise.resolve(relayAddress); - - console.log("[Network] ⏳ Attente d'un relais disponible..."); - return this.getRelayReadyPromise().then(() => { - const addr = Object.values(this.relayAddresses).find((a) => a !== ''); - if (!addr) throw new Error('Aucun relais disponible'); - return addr; - }); - } - - public printAllRelays(): void { - console.log('[Network] Adresses relais actuelles:'); - for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) { - console.log(`${wsurl} -> ${spAddress}`); - } - } - - private getRelayReadyPromise(): Promise { - if (!this.relayReadyPromise) { - this.relayReadyPromise = new Promise((resolve) => { - this.relayReadyResolver = resolve; - }); - } - return this.relayReadyPromise; - } - - private resolveRelayReady(): void { - if (this.relayReadyResolver) { - this.relayReadyResolver(); - this.relayReadyResolver = null; - this.relayReadyPromise = null; - } + // Cette méthode était utilisée pour update l'état local. + // Maintenant c'est le worker qui gère la vérité, mais on garde le cache local. + this.localRelays[url] = spAddress; } public getAllRelays() { - return this.relayAddresses; + return this.localRelays; } - public sendMessage(flag: AnkFlag, message: string) { - sendMessage(flag, message); + public async getAvailableRelayAddress(): Promise { + // On demande au worker qui a la "vraie" info temps réel + const addr = await this.worker.getAvailableRelay(); + if (addr) return addr; + + // Fallback ou attente... + throw new Error('Aucun relais disponible (NetworkWorker)'); } -} + + // --- INTERNES (CALLBACKS) --- + + private async onMessageReceived(flag: string, content: string, url: string) { + // C'est ici qu'on fait le pont : NetworkWorker -> Main -> CoreWorker + // On passe par l'instance Singleton de Services pour atteindre le CoreWorker + const services = await Services.getInstance(); + await services.dispatchToWorker(flag, content, url); + } + + private onStatusChange(url: string, status: 'OPEN' | 'CLOSED', spAddress?: string) { + if (status === 'OPEN' && spAddress) { + this.localRelays[url] = spAddress; + } else if (status === 'CLOSED') { + this.localRelays[url] = ''; + } + // On pourrait notifier l'UI ici de l'état de la connexion + } +} \ No newline at end of file diff --git a/src/services/service.ts b/src/services/service.ts index 2ab3245..b2b8adf 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1,44 +1,29 @@ -import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client'; -import Database from './database.service'; -import { storeData, retrieveData } from './storage.service'; -import { BackUp } from '../types/index'; +import * as Comlink from 'comlink'; +import { ApiReturn, Device, Member, MerkleProofResult, Process, ProcessState, SecretsStore, UserDiff, BackUp } from '../../pkg/sdk_client'; import { APP_CONFIG } from '../config/constants'; - -// Services -import { SdkService } from './core/sdk.service'; import { NetworkService } from './core/network.service'; -import { WalletService } from './domain/wallet.service'; -import { ProcessService } from './domain/process.service'; -import { CryptoService } from './domain/crypto.service'; +import type { CoreBackend } from '../workers/core.worker'; export default class Services { private static instance: Services; private static initializing: Promise | null = null; - private sdkService: SdkService; + // Worker References + private coreWorker: Comlink.Remote; + private workerInstance: Worker; + + // Services Locaux public networkService: NetworkService; - private walletService!: WalletService; - private processService!: ProcessService; - private cryptoService: CryptoService; - - private processId: string | null = null; - private stateId: string | null = null; - private membersList: Record = {}; - private notifications: any[] | null = null; - private db!: Database; - private currentBlockHeight: number = -1; - private pendingKeyRequests: Map void> = new Map(); - - public device1: boolean = false; - public device2Ready: boolean = false; private constructor() { - this.sdkService = new SdkService(); - // Utilisation de la config this.networkService = new NetworkService(APP_CONFIG.URLS.BOOTSTRAP); - this.cryptoService = new CryptoService(this.sdkService); - this.walletService = new WalletService(this.sdkService, null as any); - this.processService = new ProcessService(this.sdkService, null as any); + + // Initialisation du Core Worker + this.workerInstance = new Worker( + new URL('../workers/core.worker.ts', import.meta.url), + { type: 'module' } + ); + this.coreWorker = Comlink.wrap(this.workerInstance); } public static async getInstance(): Promise { @@ -55,902 +40,199 @@ export default class Services { } public async init(): Promise { - console.log('[Services] ⏳ Initialisation...'); - this.notifications = this.getNotifications(); - await this.sdkService.init(); - this.db = await Database.getInstance(); - this.walletService = new WalletService(this.sdkService, this.db); - this.processService = new ProcessService(this.sdkService, this.db); - - this.networkService.initRelays(); - console.log('[Services] ✅ Initialisé.'); + console.log('[Services] 🚀 Démarrage Proxy...'); + + // 1. Initialiser le Core Worker + await this.coreWorker.init(); + + // 2. Configurer les Callbacks (Le worker pilote le main thread pour le réseau) + await this.coreWorker.setCallbacks( + Comlink.proxy(this.handleWorkerNotification.bind(this)), // notifier + Comlink.proxy(this.handleWorkerNetworkSend.bind(this)), // networkSender + Comlink.proxy(this.handleWorkerRelayUpdate.bind(this)), // relayUpdater + Comlink.proxy(this.handleWorkerRelayRequest.bind(this)) // relayGetter + ); + + // 3. Initialiser le Réseau (Network Worker via Proxy) + await this.networkService.initRelays(); + + console.log('[Services] ✅ Proxy connecté au CoreWorker et NetworkService.'); } - // --- Getters & Setters --- - public get sdkClient() { - return this.sdkService.getClient(); - } - public setProcessId(id: string | null) { - this.processId = id; - } - public setStateId(id: string | null) { - this.stateId = id; - } - public getProcessId() { - return this.processId; - } - public getStateId() { - return this.stateId; + // ========================================== + // CALLBACKS DU WORKER + // ========================================== + + private handleWorkerNotification(event: string, data?: any) { + document.dispatchEvent(new CustomEvent(event, { detail: data })); } - // --- Network Proxy --- - public async connectAllRelays() { - await this.networkService.connectAllRelays(); - if (Object.keys(this.networkService.getAllRelays()).length > 0) { - try { - await this.waitForHandshakeMessage(); - } catch (e: any) { - console.error(e.message); - } - } + private handleWorkerNetworkSend(flag: string, content: string) { + this.networkService.sendMessage(flag as any, content); } - public async addWebsocketConnection(url: string): Promise { - await this.networkService.addWebsocketConnection(url); - } - public getAllRelays() { - const relays = this.networkService.getAllRelays(); - return Object.entries(relays).map(([wsurl, spAddress]) => ({ wsurl, spAddress })); - } - public updateRelay(url: string, sp: string) { + + private handleWorkerRelayUpdate(url: string, sp: string) { this.networkService.updateRelay(url, sp); } - public getSpAddress(url: string) { - return this.networkService.getAllRelays()[url]; + + private async handleWorkerRelayRequest(): Promise { + return await this.networkService.getAvailableRelayAddress(); } + + // ========================================== + // DISPATCH WEBSOCKET VERS WORKER + // ========================================== + public async dispatchToWorker(flag: string, content: string, url: string) { + switch (flag) { + case 'Handshake': await this.coreWorker.handleHandshakeMsg(url, content); break; + case 'NewTx': await this.coreWorker.parseNewTx(content); break; + case 'Cipher': await this.coreWorker.parseCipher(content); break; + case 'Commit': await this.coreWorker.handleCommitError(content); break; + } + } + + // ========================================== + // PROXY - PROPERTIES PUBLIC + // ========================================== + public async getDevice1() { return await this.coreWorker.getDevice1(); } + public async getDevice2Ready() { return await this.coreWorker.getDevice2Ready(); } + + // NOTE: getSdkClient renvoie null car l'objet WASM n'est pas transférable hors du worker. + // Toute la logique utilisant le client doit être dans le worker. + public get sdkClient() { return null; } + + public async setProcessId(id: string | null) { return await this.coreWorker.setProcessId(id); } + public async setStateId(id: string | null) { return await this.coreWorker.setStateId(id); } + public async getProcessId() { return await this.coreWorker.getProcessId(); } + public async getStateId() { return await this.coreWorker.getStateId(); } + public async resetState() { return await this.coreWorker.resetState(); } + + // ========================================== + // PROXY - NETWORK PROXY + // ========================================== + public async connectAllRelays() { await this.networkService.connectAllRelays(); } + public async addWebsocketConnection(url: string) { await this.networkService.addWebsocketConnection(url); } + public getAllRelays() { return this.networkService.getAllRelays(); } + public updateRelay(url: string, sp: string) { this.networkService.updateRelay(url, sp); } + public getSpAddress(url: string) { return this.networkService.getAllRelays()[url]; } + // Délégation directe au NetworkService public printAllRelays() { - this.networkService.printAllRelays(); - } - - // --- Wallet Proxy --- - public isPaired() { - return this.walletService.isPaired(); - } - public getAmount() { - return this.walletService.getAmount(); - } - public getDeviceAddress() { - return this.walletService.getDeviceAddress(); - } - public dumpDeviceFromMemory() { - return this.walletService.dumpDeviceFromMemory(); - } - public dumpNeuteredDevice() { - return this.walletService.dumpNeuteredDevice(); - } - public getPairingProcessId() { - return this.walletService.getPairingProcessId(); - } - public async getDeviceFromDatabase() { - return this.walletService.getDeviceFromDatabase(); - } - public restoreDevice(d: Device) { - this.walletService.restoreDevice(d); - } - public pairDevice(pid: string, list: string[]) { - this.walletService.pairDevice(pid, list); - } - public async unpairDevice() { - await this.walletService.unpairDevice(); - } - public async saveDeviceInDatabase(d: Device) { - await this.walletService.saveDeviceInDatabase(d); - } - public async createNewDevice() { - return this.walletService.createNewDevice(this.currentBlockHeight > 0 ? this.currentBlockHeight : 0); - } - public async dumpWallet() { - return this.walletService.dumpWallet(); - } - public async getMemberFromDevice() { - return this.walletService.getMemberFromDevice(); - } - - // --- Process Proxy --- - public async getProcess(id: string) { - return this.processService.getProcess(id); - } - public async getProcesses() { - return this.processService.getProcesses(); - } - public async restoreProcessesFromDB() { - await this.processService.getProcesses(); - } - public getLastCommitedState(p: Process) { - return this.processService.getLastCommitedState(p); - } - public getUncommitedStates(p: Process) { - return this.processService.getUncommitedStates(p); - } - public getStateFromId(p: Process, id: string) { - return this.processService.getStateFromId(p, id); - } - public getRoles(p: Process) { - return this.processService.getRoles(p); - } - public getLastCommitedStateIndex(p: Process) { - return this.processService.getLastCommitedStateIndex(p); - } - public async batchSaveProcessesToDb(p: Record) { - return this.processService.batchSaveProcesses(p); - } - - // --- Helpers Crypto Proxy --- - public decodeValue(val: number[]) { - return this.sdkService.decodeValue(val); - } - public hexToBlob(hex: string) { - return this.cryptoService.hexToBlob(hex); - } - public hexToUInt8Array(hex: string) { - return this.cryptoService.hexToUInt8Array(hex); - } - public async blobToHex(blob: Blob) { - return this.cryptoService.blobToHex(blob); - } - public getHashForFile(c: string, l: string, f: any) { - return this.cryptoService.getHashForFile(c, l, f); - } - public getMerkleProofForFile(s: ProcessState, a: string) { - return this.cryptoService.getMerkleProofForFile(s, a); - } - public validateMerkleProof(p: MerkleProofResult, h: string) { - return this.cryptoService.validateMerkleProof(p, h); - } - private splitData(obj: Record) { - return this.cryptoService.splitData(obj); - } - - // --- Membres --- - public getAllMembers() { - return this.membersList; - } - public getAllMembersSorted() { - return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))); - } - public async ensureMembersAvailable(): Promise { - if (Object.keys(this.membersList).length > 0) return; - console.warn('[Services] Tentative de récupération des membres...'); - await this.connectAllRelays(); - } - public getAddressesForMemberId(memberId: string): string[] | null { - if (!this.membersList[memberId]) return null; - return this.membersList[memberId].sp_addresses; - } - public compareMembers(memberA: string[], memberB: string[]): boolean { - if (!memberA || !memberB) return false; - if (memberA.length !== memberB.length) return false; - return memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item)); - } - - // --- Utilitaires --- - public createFaucetMessage() { - return this.sdkClient.create_faucet_msg(); - } - - public isChildRole(parent: any, child: any): boolean { - try { - this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child)); - return true; - } catch (e) { - console.error(e); - return false; - } - } - - public resetState() { - this.device1 = false; - this.device2Ready = false; - } - - // --- Logique Handshake --- - 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.currentBlockHeight = handshakeMsg.chain_tip; - - if (!this.isPaired()) { - console.log(`[Services] ⏳ Non pairé. Le Handshake de ${url} est en pause...`); - while (!this.isPaired()) { - await new Promise(r => setTimeout(r, 500)); - } - console.log(`[Services] ▶️ Appareil pairé ! Reprise du traitement Handshake de ${url}.`); - } - - this.updateDeviceBlockHeight(); - - if (handshakeMsg.peers_list) { - this.membersList = { ...this.membersList, ...handshakeMsg.peers_list as Record }; - } - - if (handshakeMsg.processes_list) { - this.syncProcessesFromHandshake(handshakeMsg.processes_list); - } - - } catch(e) { - console.error("Handshake Error", e); - } - } - - private async waitForHandshakeMessage(timeoutMs = APP_CONFIG.TIMEOUTS.HANDSHAKE): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - if (Object.keys(this.membersList).length > 0 || Object.values(this.networkService.getAllRelays()).some((a) => a !== '')) return; - await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.POLLING_INTERVAL)); - } - throw new Error('Timeout waiting for handshake'); - } - - public async updateDeviceBlockHeight() { - if (this.currentBlockHeight <= 0) return; - const device = await this.walletService.getDeviceFromDatabase(); - if (!device) return; - - if (device.sp_wallet.birthday === 0) { - device.sp_wallet.birthday = this.currentBlockHeight; - device.sp_wallet.last_scan = this.currentBlockHeight; - await this.walletService.saveDeviceInDatabase(device); - this.walletService.restoreDevice(device); - } else if (device.sp_wallet.last_scan < this.currentBlockHeight) { - console.log(`[Services] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`); - try { - await this.sdkClient.scan_blocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT); - const updatedDevice = this.walletService.dumpDeviceFromMemory(); - await this.walletService.saveDeviceInDatabase(updatedDevice); - } catch (e) { - console.error('Scan error', e); - } - } - } - - // --- Logique Métier --- - public async getMyProcesses(): Promise { - try { - const pid = this.getPairingProcessId(); - return await this.processService.getMyProcesses(pid); - } catch (e) { - return null; - } - } - - public async ensureConnections(process: Process, stateId: string | null = null): Promise { - console.info(`[ConnectionCheck] 🔄 Check connexions (StateID: ${stateId || 'default'})`); - if (!process) return; - - let state: ProcessState | null = null; - if (stateId) state = this.processService.getStateFromId(process, stateId); - if (!state && process.states.length >= 2) state = process.states[process.states.length - 2]; - if (!state) return; - - await this.ensureMembersAvailable(); - const members = new Set(); - - if (state.roles) { - for (const role of Object.values(state.roles)) { - for (const memberId of role.members) { - const addrs = this.getAddressesForMemberId(memberId); - if (addrs) members.add({ sp_addresses: addrs }); - } - } - } - - if (members.size === 0) { - let publicData: Record | null = null; - for (let i = process.states.length - 1; i >= 0; i--) { - const s = process.states[i]; - if (s.public_data && s.public_data['pairedAddresses']) { - publicData = s.public_data; - break; - } - } - if (publicData && publicData['pairedAddresses']) { - const decoded = this.decodeValue(publicData['pairedAddresses']); - if (decoded) members.add({ sp_addresses: decoded }); - } - } - - if (members.size === 0) return; - - const unconnected = new Set(); - const myAddress = this.getDeviceAddress(); - for (const member of Array.from(members)) { - if (!member.sp_addresses) continue; - for (const address of member.sp_addresses) { - if (address === myAddress) continue; - if ((await this.getSecretForAddress(address)) === null) unconnected.add(address); - } - } - - if (unconnected.size > 0) { - console.log(`[ConnectionCheck] 📡 ${unconnected.size} non connectés. Connexion...`); - await this.connectAddresses(Array.from(unconnected)); - } - } - - public async connectAddresses(addresses: string[]): Promise { - if (addresses.length === 0) return null; - const feeRate = APP_CONFIG.FEE_RATE; - try { - return this.sdkClient.create_transaction(addresses, feeRate); - } catch (error: any) { - if (String(error).includes('Insufficient funds')) { - await this.getTokensFromFaucet(); - return this.sdkClient.create_transaction(addresses, feeRate); - } else { - throw error; - } - } - } - - private async getTokensFromFaucet(): Promise { - console.log('[Services] 🚰 Demande Faucet...'); - const availableAmt = this.getAmount(); - const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10); - if (availableAmt < target) { - const msg = this.sdkClient.create_faucet_msg(); - this.networkService.sendMessage('Faucet', msg); - let attempts = 3; - while (attempts > 0) { - if (this.getAmount() >= target) return; - attempts--; - await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY)); - } - throw new Error('Montant insuffisant après faucet'); - } - } - - private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap) { - if (!newProcesses || Object.keys(newProcesses).length === 0) return; - console.log(`[Services] Synchro ${Object.keys(newProcesses).length} processus...`); - - const toSave: Record = {}; - const currentProcesses = await this.getProcesses(); - - if (Object.keys(currentProcesses).length === 0) { - await this.processService.batchSaveProcesses(newProcesses); - } else { - for (const [processId, process] of Object.entries(newProcesses)) { - const existing = currentProcesses[processId]; - if (existing) { - let newStates: string[] = []; - let newRoles: Record[] = []; - - for (const state of process.states) { - if (!state || !state.state_id) continue; - - if (state.state_id === APP_CONFIG.EMPTY_32_BYTES) { - const existingTip = existing.states[existing.states.length - 1].commited_in; - if (existingTip !== state.commited_in) { - existing.states.pop(); - existing.states.push(state); - toSave[processId] = existing; - } - } else if (!this.processService.getStateFromId(existing, state.state_id)) { - const existingLast = existing.states.pop(); - if (existingLast) { - existing.states.push(state); - existing.states.push(existingLast); - toSave[processId] = existing; - if (this.rolesContainsUs(state.roles)) { - newStates.push(state.state_id); - newRoles.push(state.roles); - } - } - } else { - const existingState = this.processService.getStateFromId(existing, state.state_id); - if (existingState && (!existingState.keys || Object.keys(existingState.keys).length === 0)) { - if (this.rolesContainsUs(state.roles)) { - newStates.push(state.state_id); - newRoles.push(state.roles); - } - } - } - } - - if (newStates.length > 0) { - await this.ensureConnections(existing); - await this.requestDataFromPeers(processId, newStates, newRoles); - } - } else { - toSave[processId] = process; - } - } - if (Object.keys(toSave).length > 0) { - await this.processService.batchSaveProcesses(toSave); - } - } - document.dispatchEvent(new CustomEvent('processes-updated')); - } - - public async createPairingProcess(userName: string, pairWith: string[]): Promise { - if (this.isPaired()) throw new Error('Déjà appairé'); - const myAddress = this.getDeviceAddress(); - pairWith.push(myAddress); - const privateData = { description: 'pairing', counter: 0 }; - const publicData = { memberPublicName: userName, pairedAddresses: pairWith }; - const validation_fields = [...Object.keys(privateData), ...Object.keys(publicData), 'roles']; - const roles = { - pairing: { - members: [], - validation_rules: [{ quorum: 1.0, fields: validation_fields, min_sig_member: 1.0 }], - storages: [APP_CONFIG.URLS.STORAGE], - }, - }; - return this.createProcess(privateData, publicData, roles); - } - - public async createProcess(privateData: any, publicData: any, roles: any, feeRate = APP_CONFIG.FEE_RATE): Promise { - const relay = await this.networkService.getAvailableRelayAddress(); - const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); - const members = this.membersList; - try { - return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members); - } catch (e: any) { - if (String(e).includes('Insufficient funds')) { - await this.getTokensFromFaucet(); - return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members); - } - throw e; - } - } - - private async attemptCreateProcess(priv: any, roles: any, pub: any, relay: string, fee: number, members: any): Promise { - const res = this.sdkClient.create_new_process(priv, roles, pub, relay, fee, members); - if (res.updated_process) { - await this.ensureConnections(res.updated_process.current_process); - } - return res; - } - - public async updateProcess(processId: string, newData: any, privateFields: string[], roles: any): Promise { - const process = await this.processService.getProcess(processId); - if (!process) throw new Error('Process not found'); - - let lastState = this.processService.getLastCommitedState(process); - let currentProcess = process; - - if (!lastState) { - const first = process.states[0]; - if (this.rolesContainsUs(first.roles)) { - const appRes = await this.approveChange(processId, first.state_id); - await this.handleApiReturn(appRes); - const prdRes = await this.createPrdUpdate(processId, first.state_id); - await this.handleApiReturn(prdRes); - } else if (first.validation_tokens.length > 0) { - const res = await this.createPrdUpdate(processId, first.state_id); - await this.handleApiReturn(res); - } - const updated = await this.processService.getProcess(processId); - if (updated) currentProcess = updated; - lastState = this.processService.getLastCommitedState(currentProcess); - if (!lastState) throw new Error('Still no commited state'); - } - - const lastStateIndex = this.getLastCommitedStateIndex(currentProcess); - if (lastStateIndex === null) throw new Error('Index commited introuvable'); - - const privateData: any = {}; - const publicData: any = {}; - - 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; - } - let isPrivate = false; - for (let i = lastStateIndex; i >= 0; i--) { - if (currentProcess.states[i].pcd_commitment[field]) { - privateData[field] = newData[field]; - isPrivate = true; - break; - } - } - if (!isPrivate) publicData[field] = newData[field]; - } - - const finalRoles = roles || this.processService.getRoles(currentProcess); - const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); - - const res = this.sdkClient.update_process(currentProcess, encodedPrivateData, finalRoles, encodedPublicData, this.membersList); - if (res.updated_process) await this.ensureConnections(res.updated_process.current_process); - return res; - } - - // public async confirmPairing() { - // console.log('[Services] Confirm Pairing...'); - // const pid = this.walletService.getPairingProcessId(); - // const process = await this.processService.getProcess(pid); - // if (!process) return; - - // let state = this.processService.getLastCommitedState(process); - // if (!state && process.states.length > 0) state = process.states[process.states.length - 1]; - // if (!state) return; - - // const encodedAddr = state.public_data['pairedAddresses']; - // if (!encodedAddr) return; - - // const addresses = this.decodeValue(encodedAddr); - // if (!addresses || addresses.length === 0) return; - - // this.sdkClient.unpair_device(); - // this.walletService.pairDevice(pid, addresses); - - // if (this.walletService.isPaired()) { - // const d = this.walletService.dumpDeviceFromMemory(); - // if (!this.walletService.isPaired()) d.pairing_process_commitment = pid; - // await this.walletService.saveDeviceInDatabase(d); - // console.log('✅ Pairing confirmed & Saved'); - // } - // } - - private async prepareProcessData(priv: any, pub: any) { - const p1 = this.splitData(priv); - const p2 = this.splitData(pub); - return { - encodedPrivateData: { ...this.sdkClient.encode_json(p1.jsonCompatibleData), ...this.sdkClient.encode_binary(p1.binaryData) }, - encodedPublicData: { ...this.sdkClient.encode_json(p2.jsonCompatibleData), ...this.sdkClient.encode_binary(p2.binaryData) }, - }; - } - - // API Methods - public async createPrdUpdate(pid: string, sid: string) { - const p = await this.getProcess(pid); - await this.ensureConnections(p!); - return this.sdkClient.create_update_message(p, sid, this.membersList); - } - public async createPrdResponse(pid: string, sid: string) { - const p = await this.getProcess(pid); - return this.sdkClient.create_response_prd(p, sid, this.membersList); - } - public async approveChange(pid: string, sid: string) { - const p = await this.getProcess(pid); - const res = this.sdkClient.validate_state(p, sid, this.membersList); - if (res.updated_process) await this.ensureConnections(res.updated_process.current_process); - return res; - } - public async rejectChange(pid: string, sid: string) { - const p = await this.getProcess(pid); - return this.sdkClient.refuse_state(p, sid); - } - public async requestDataFromPeers(pid: string, sids: string[], roles: any) { - const res = this.sdkClient.request_data(pid, sids, roles, this.membersList); - await this.handleApiReturn(res); - } - - public async resetDevice() { - // console.warn("[Services:resetDevice] ⚠️ RÉINITIALISATION COMPLÈTE de l'appareil et de la BDD..."); - this.sdkClient.reset_device(); - - // Clear all stores - await this.db.clearMultipleStores(['wallet', 'shared_secrets', 'unconfirmed_secrets', 'processes', 'diffs']); - // console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.'); - } - - public async handleApiReturn(res: ApiReturn) { - if (!res || Object.keys(res).length === 0) return; - try { - const txData = (res.partial_tx ? await this.handlePartialTx(res.partial_tx) : null) || res.new_tx_to_send; - if (txData && txData.transaction.length != 0) { - this.networkService.sendMessage('NewTx', JSON.stringify(txData)); - await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY)); - } - if (res.secrets) await this.handleSecrets(res.secrets); - if (res.updated_process) await this.handleUpdatedProcess(res.updated_process); - if (res.push_to_storage) await this.handlePushToStorage(res.push_to_storage); - if (res.commit_to_send) this.networkService.sendMessage('Commit', JSON.stringify(res.commit_to_send)); - if (res.ciphers_to_send) for (const c of res.ciphers_to_send) this.networkService.sendMessage('Cipher', c); - } catch (e) { - console.error('ApiReturn Error:', e); - } - } - - private async handlePartialTx(partialTx: any): Promise { - try { - return this.sdkClient.sign_transaction(partialTx).new_tx_to_send; - } catch (e) { - return null; - } - } - - private async handleSecrets(secrets: any) { - const { unconfirmed_secrets, shared_secrets } = secrets; - - const unconfirmedList = unconfirmed_secrets && unconfirmed_secrets.length > 0 ? unconfirmed_secrets : []; - const sharedList = shared_secrets && Object.keys(shared_secrets).length > 0 - ? Object.entries(shared_secrets).map(([key, value]) => ({ key, value })) - : []; - - if (unconfirmedList.length > 0 || sharedList.length > 0) { - try { - await this.db.saveSecretsBatch(unconfirmedList, sharedList); - } catch (e) { - console.error('[Services:handleSecrets] 💥 Échec de sauvegarde batch des secrets:', e); - } - } - } - - private async handleUpdatedProcess(updated: any) { - const pid = updated.process_id; - if (updated.encrypted_data) { - for (const [h, c] of Object.entries(updated.encrypted_data as Record)) await this.saveBlobToDb(h, this.hexToBlob(c)); - } - await this.processService.saveProcessToDb(pid, updated.current_process); - if (updated.diffs) await this.saveDiffsToDb(updated.diffs); - - this._resolvePendingKeyRequests(pid, updated.current_process); - const dev = await this.walletService.getDeviceFromDatabase(); - if (dev && dev.pairing_process_commitment === pid) { - const last = updated.current_process.states[updated.current_process.states.length - 1]; - // if (last?.public_data['pairedAddresses']) await this.confirmPairing(); - } - } - - public async saveDiffsToDb(diffs: UserDiff[]) { - await this.db.saveDiffs(diffs); - } - - private _resolvePendingKeyRequests(processId: string, process: Process) { - if (this.pendingKeyRequests.size === 0) return; - for (const state of process.states) { - if (!state.keys) continue; - for (const [attr, key] of Object.entries(state.keys)) { - const rid = `${processId}_${state.state_id}_${attr}`; - if (this.pendingKeyRequests.has(rid)) { - this.pendingKeyRequests.get(rid)?.(key as string); - this.pendingKeyRequests.delete(rid); - } - } - } - } - - private async handlePushToStorage(hashes: string[]) { - for (const hash of hashes) { - try { - const blob = await this.getBlobFromDb(hash); - const diff = await this.getDiffByValue(hash); - if (blob && diff) await this.saveDataToStorage(diff.storages, hash, blob, null); - } catch (e) { - console.error('Push error', e); - } - } - } - - public async handleCommitError(response: string) { - const content = JSON.parse(response); - const errorMsg = content.error['GenericError']; - if (!['State is identical to the previous state', 'Not enough valid proofs'].includes(errorMsg)) { - setTimeout(() => this.networkService.sendMessage('Commit', JSON.stringify(content)), APP_CONFIG.TIMEOUTS.RETRY_DELAY); - } - } - - public rolesContainsUs(roles: any) { - return this.processService.rolesContainsMember(roles, this.getPairingProcessId()); - } - - public async getSecretForAddress(address: string): Promise { - return await this.db.getSharedSecret(address); - } - - public async getAllDiffs(): Promise> { - return await this.db.getAllDiffs(); - } - - public async getDiffByValue(value: string): Promise { - return await this.db.getDiff(value); - } - - public async getAllSecrets(): Promise { - return await this.db.getAllSecrets(); - } - - // Storage & DB - public async saveBlobToDb(h: string, d: Blob) { - await this.db.saveBlob(h, d); - } - public async getBlobFromDb(h: string) { - return await this.db.getBlob(h); - } - public async fetchValueFromStorage(h: string) { - return retrieveData([APP_CONFIG.URLS.STORAGE], h); - } - public async saveDataToStorage(s: string[], h: string, d: Blob, ttl: number | null) { - return storeData(s, h, d, ttl); - } - - // Helpers - public getProcessName(p: Process) { - const pub = this.getPublicData(p); - if (pub && pub['processName']) return this.decodeValue(pub['processName']); - return null; - } - public getPublicData(p: Process) { - const last = this.getLastCommitedState(p); - return last ? last.public_data : p.states[0]?.public_data || null; - } - - // UI helpers - public getNotifications() { - return this.notifications; - } - public setNotifications(n: any[]) { - this.notifications = n; - } - - async parseCipher(msg: string) { - try { - const res = this.sdkClient.parse_cipher(msg, this.membersList, await this.getProcesses()); - await this.handleApiReturn(res); - } catch (e) { - console.error('Cipher Error', e); - } - } - - async parseNewTx(msg: string) { - const parsed = JSON.parse(msg); - if (parsed.error) return; - - const prevouts = this.sdkClient.get_prevouts(parsed.transaction); - for (const p of Object.values(await this.getProcesses())) { - const tip = p.states[p.states.length - 1].commited_in; - if (prevouts.includes(tip)) { - const newTip = this.sdkClient.get_txid(parsed.transaction); - const newStateId = this.sdkClient.get_opreturn(parsed.transaction); - const updated = this.sdkClient.process_commit_new_state(p, newStateId, newTip); - break; - } - } - - try { - const res = this.sdkClient.parse_new_tx(msg, 0, this.membersList); - if (res && (res.partial_tx || res.new_tx_to_send || res.secrets || res.updated_process)) { - await this.handleApiReturn(res); - const d = this.dumpDeviceFromMemory(); - const old = await this.getDeviceFromDatabase(); - if (old && old.pairing_process_commitment) d.pairing_process_commitment = old.pairing_process_commitment; - await this.saveDeviceInDatabase(d); - } - } catch (e) {} - } - - public updateMemberPublicName(pid: string, name: string) { - return this.updateProcess(pid, { memberPublicName: name }, [], null); - } - - public async importJSON(backup: BackUp) { - await this.resetDevice(); - await this.walletService.saveDeviceInDatabase(backup.device); - this.walletService.restoreDevice(backup.device); - await this.processService.batchSaveProcesses(backup.processes); - await this.restoreSecretsFromBackUp(backup.secrets); - } - public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { - const sharedList = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value })); - await this.db.saveSecretsBatch(secretsStore.unconfirmed_secrets, sharedList); - await this.restoreSecretsFromDB(); - } - public async restoreSecretsFromDB() { - const secretsStore = await this.db.getAllSecrets(); - this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore)); - } - public async createBackUp() { - const device = await this.walletService.getDeviceFromDatabase(); - if (!device) return null; - return { device, processes: await this.processService.getProcesses(), secrets: await this.getAllSecrets() }; - } - - public async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { - console.groupCollapsed(`[Services:decryptAttribute] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`); - - try { - let hash: string | null | undefined = state.pcd_commitment[attribute]; - let key: string | null | undefined = state.keys[attribute]; - const pairingProcessId = this.getPairingProcessId(); - - if (!hash) { - console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); - return null; - } - - if (!key) { - if (!this._checkAccess(state, attribute, pairingProcessId)) { - console.log(`⛔ Accès non autorisé. Abandon.`); - return null; - } - const result = await this._fetchMissingKey(processId, state, attribute); - hash = result.hash; - key = result.key; - } - - if (hash && key) { - const blob = await this.getBlobFromDb(hash); - if (!blob) { - console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`); - return null; - } - - try { - const buf = await blob.arrayBuffer(); - const cipher = new Uint8Array(buf); - const keyUIntArray = this.hexToUInt8Array(key); - - const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher); - if (!clear) throw new Error('decrypt_data returned null'); - - const decoded = this.sdkClient.decode_value(clear); - console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); - return decoded; - } catch (e) { - console.error(`💥 Échec du déchiffrement: ${e}`); - return null; - } - } - return null; - } catch (error) { - console.error(`💥 Erreur:`, error); - return null; - } finally { - console.groupEnd(); - } - } - - private _checkAccess(state: ProcessState, attribute: string, pairingProcessId: string): boolean { - const roles = state.roles; - return Object.values(roles).some((role) => { - const isMember = role.members.includes(pairingProcessId); - if (!isMember) return false; - return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute)); - }); - } - - private async _fetchMissingKey(processId: string, state: ProcessState, attribute: string): Promise<{ hash: string | null; key: string | null }> { - try { - const process = await this.getProcess(processId); - if (!process) return { hash: null, key: null }; - - await this.ensureConnections(process); - await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); - - const requestId = `${processId}_${state.state_id}_${attribute}`; - const keyRequestPromise = new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingKeyRequests.delete(requestId); - reject(new Error(`Timeout waiting for key: ${attribute}`)); - }, APP_CONFIG.TIMEOUTS.KEY_REQUEST); - - this.pendingKeyRequests.set(requestId, (key: string) => { - clearTimeout(timeout); - resolve(key); - }); - }); - - const receivedKey = await keyRequestPromise; - const updatedProcess = await this.getProcess(processId); - if (!updatedProcess) return { hash: null, key: null }; - - const updatedState = this.getStateFromId(updatedProcess, state.state_id); - const updatedHash = updatedState ? updatedState.pcd_commitment[attribute] : state.pcd_commitment[attribute]; - - return { hash: updatedHash, key: receivedKey }; - } catch (e) { - return { hash: null, key: null }; - } - } -} - + // Si NetworkService ne l'expose pas directement dans sa version proxy, on l'ajoute ou on log ici + console.log('Relays:', this.networkService.getAllRelays()); + } + + // ========================================== + // PROXY - WALLET + // ========================================== + public async isPaired() { return await this.coreWorker.isPaired(); } + public async getAmount() { return await this.coreWorker.getAmount(); } + public async getDeviceAddress() { return await this.coreWorker.getDeviceAddress(); } + public async dumpDeviceFromMemory() { return await this.coreWorker.dumpDeviceFromMemory(); } + public async dumpNeuteredDevice() { return await this.coreWorker.dumpNeuteredDevice(); } + public async getPairingProcessId() { return await this.coreWorker.getPairingProcessId(); } + public async getDeviceFromDatabase() { return await this.coreWorker.getDeviceFromDatabase(); } + public async restoreDevice(d: Device) { await this.coreWorker.restoreDevice(d); } + public async pairDevice(pid: string, list: string[]) { await this.coreWorker.pairDevice(pid, list); } + public async unpairDevice() { await this.coreWorker.unpairDevice(); } + public async saveDeviceInDatabase(d: Device) { await this.coreWorker.saveDeviceInDatabase(d); } + public async createNewDevice() { return await this.coreWorker.createNewDevice(); } + public async dumpWallet() { return await this.coreWorker.dumpWallet(); } + public async getMemberFromDevice() { return await this.coreWorker.getMemberFromDevice(); } + + // ========================================== + // PROXY - PROCESS + // ========================================== + public async getProcess(id: string) { return await this.coreWorker.getProcess(id); } + public async getProcesses() { return await this.coreWorker.getProcesses(); } + public async restoreProcessesFromDB() { await this.coreWorker.restoreProcessesFromDB(); } + public async getLastCommitedState(p: Process) { return await this.coreWorker.getLastCommitedState(p); } + public async getUncommitedStates(p: Process) { return await this.coreWorker.getUncommitedStates(p); } + public async getStateFromId(p: Process, id: string) { return await this.coreWorker.getStateFromId(p, id); } + public async getRoles(p: Process) { return await this.coreWorker.getRoles(p); } + public async getLastCommitedStateIndex(p: Process) { return await this.coreWorker.getLastCommitedStateIndex(p); } + public async batchSaveProcessesToDb(p: Record) { return await this.coreWorker.batchSaveProcessesToDb(p); } + + // ========================================== + // PROXY - CRYPTO + // ========================================== + public async decodeValue(val: number[]) { return await this.coreWorker.decodeValue(val); } + public async hexToBlob(hex: string) { return await this.coreWorker.hexToBlob(hex); } + public async hexToUInt8Array(hex: string) { return await this.coreWorker.hexToUInt8Array(hex); } + public async blobToHex(blob: Blob) { return await this.coreWorker.blobToHex(blob); } + public async getHashForFile(c: string, l: string, f: any) { return await this.coreWorker.getHashForFile(c, l, f); } + public async getMerkleProofForFile(s: ProcessState, a: string) { return await this.coreWorker.getMerkleProofForFile(s, a); } + public async validateMerkleProof(p: MerkleProofResult, h: string) { return await this.coreWorker.validateMerkleProof(p, h); } + + // ========================================== + // PROXY - MEMBERS + // ========================================== + public async getAllMembers() { return await this.coreWorker.getAllMembers(); } + public async getAllMembersSorted() { return await this.coreWorker.getAllMembersSorted(); } + public async ensureMembersAvailable() { await this.coreWorker.ensureMembersAvailable(); } + public async getAddressesForMemberId(memberId: string) { return await this.coreWorker.getAddressesForMemberId(memberId); } + public async compareMembers(mA: string[], mB: string[]) { return await this.coreWorker.compareMembers(mA, mB); } + + // ========================================== + // PROXY - UTILS + // ========================================== + public async createFaucetMessage() { return await this.coreWorker.createFaucetMessage(); } + public async isChildRole(parent: any, child: any) { return await this.coreWorker.isChildRole(parent, child); } + + // ========================================== + // PROXY - LOGIQUE METIER & API + // ========================================== + public async getMyProcesses() { return await this.coreWorker.getMyProcesses(); } + public async ensureConnections(p: Process, sId?: string) { await this.coreWorker.ensureConnections(p, sId || null); } + public async connectAddresses(addr: string[]) { return await this.coreWorker.connectAddresses(addr); } + + public async createPairingProcess(name: string, pairWith: string[]) { return await this.coreWorker.createPairingProcess(name, pairWith); } + public async createProcess(priv: any, pub: any, roles: any) { return await this.coreWorker.createProcess(priv, pub, roles); } + public async updateProcess(pid: string, newData: any, priv: string[], roles: any) { return await this.coreWorker.updateProcess(pid, newData, priv, roles); } + + public async createPrdUpdate(pid: string, sid: string) { return await this.coreWorker.createPrdUpdate(pid, sid); } + public async createPrdResponse(pid: string, sid: string) { return await this.coreWorker.createPrdResponse(pid, sid); } + public async approveChange(pid: string, sid: string) { return await this.coreWorker.approveChange(pid, sid); } + public async rejectChange(pid: string, sid: string) { return await this.coreWorker.rejectChange(pid, sid); } + public async requestDataFromPeers(pid: string, sids: string[], roles: any) { await this.coreWorker.requestDataFromPeers(pid, sids, roles); } + + public async resetDevice() { await this.coreWorker.resetDevice(); } + public async handleApiReturn(res: ApiReturn) { await this.coreWorker.handleApiReturn(res); } + public async saveDiffsToDb(diffs: UserDiff[]) { await this.coreWorker.saveDiffsToDb(diffs); } + public async handleCommitError(res: string) { await this.coreWorker.handleCommitError(res); } + + public async rolesContainsUs(roles: any) { return await this.coreWorker.rolesContainsUs(roles); } + public async getSecretForAddress(addr: string) { return await this.coreWorker.getSecretForAddress(addr); } + public async getAllDiffs() { return await this.coreWorker.getAllDiffs(); } + public async getDiffByValue(val: string) { return await this.coreWorker.getDiffByValue(val); } + public async getAllSecrets() { return await this.coreWorker.getAllSecrets(); } + + // ========================================== + // PROXY - STORAGE & DB + // ========================================== + public async saveBlobToDb(h: string, d: Blob) { await this.coreWorker.saveBlobToDb(h, d); } + public async getBlobFromDb(h: string) { return await this.coreWorker.getBlobFromDb(h); } + public async fetchValueFromStorage(h: string) { return await this.coreWorker.fetchValueFromStorage(h); } + public async saveDataToStorage(s: string[], h: string, d: Blob, ttl: number | null) { return await this.coreWorker.saveDataToStorage(s, h, d, ttl); } + + // ========================================== + // PROXY - HELPERS UI & DATA + // ========================================== + public async getProcessName(p: Process) { return await this.coreWorker.getProcessName(p); } + public async getPublicData(p: Process) { return await this.coreWorker.getPublicData(p); } + public async getNotifications() { return await this.coreWorker.getNotifications(); } + public async setNotifications(n: any[]) { await this.coreWorker.setNotifications(n); } + + public async parseCipher(msg: string) { await this.coreWorker.parseCipher(msg); } + public async parseNewTx(msg: string) { await this.coreWorker.parseNewTx(msg); } + public async updateMemberPublicName(pid: string, name: string) { return await this.coreWorker.updateMemberPublicName(pid, name); } + + // ========================================== + // PROXY - BACKUP & DECRYPT + // ========================================== + public async importJSON(b: BackUp) { await this.coreWorker.importJSON(b); } + public async restoreSecretsFromBackUp(s: SecretsStore) { await this.coreWorker.restoreSecretsFromBackUp(s); } + public async restoreSecretsFromDB() { await this.coreWorker.restoreSecretsFromDB(); } + public async createBackUp() { return await this.coreWorker.createBackUp(); } + + public async decryptAttribute(pid: string, s: ProcessState, attr: string) { return await this.coreWorker.decryptAttribute(pid, s, attr); } +} \ No newline at end of file diff --git a/src/services/websockets.service.ts b/src/services/websockets.service.ts deleted file mode 100755 index 117e2fa..0000000 --- a/src/services/websockets.service.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { AnkFlag } from '../../pkg/sdk_client'; // Vérifie le chemin vers pkg -import Services from './service'; -import { APP_CONFIG } from '../config/constants'; - -let ws: WebSocket | null = null; -let messageQueue: string[] = []; -let reconnectInterval = APP_CONFIG.TIMEOUTS.RETRY_DELAY; -const MAX_RECONNECT_INTERVAL = APP_CONFIG.TIMEOUTS.WS_RECONNECT_MAX; -let isConnecting = false; -let urlReference: string = ''; -let pingIntervalId: any = null; - -export async function initWebsocket(url: string) { - urlReference = url; - connect(); -} - -function connect() { - if (isConnecting || (ws && ws.readyState === WebSocket.OPEN)) return; - isConnecting = true; - - console.log(`[WS] 🔌 Tentative de connexion à ${urlReference}...`); - ws = new WebSocket(urlReference); - - ws.onopen = async () => { - console.log('[WS] ✅ Connexion établie !'); - isConnecting = false; - reconnectInterval = APP_CONFIG.TIMEOUTS.RETRY_DELAY; // Reset du délai - - // Démarrer le Heartbeat (Ping pour garder la connexion vivante) - startHeartbeat(); - - // Vider la file d'attente (messages envoyés pendant la coupure) - while (messageQueue.length > 0) { - const message = messageQueue.shift(); - if (message) ws?.send(message); - } - }; - - ws.onmessage = (event) => { - const msgData = event.data; - if (typeof msgData === 'string') { - (async () => { - try { - const parsedMessage = JSON.parse(msgData); - const services = await Services.getInstance(); - - // Gestion des messages - switch (parsedMessage.flag) { - case 'Handshake': - await services.handleHandshakeMsg(urlReference, parsedMessage.content); - break; - case 'NewTx': - await services.parseNewTx(parsedMessage.content); - break; - case 'Cipher': - await services.parseCipher(parsedMessage.content); - break; - case 'Commit': - await services.handleCommitError(parsedMessage.content); - break; - // Ajoute d'autres cas si nécessaire - default: - // console.log('[WS] Message reçu:', parsedMessage.flag); - } - } catch (error) { - console.error('[WS] Erreur traitement message:', error); - } - })(); - } - }; - - ws.onerror = (event) => { - console.error('[WS] 💥 Erreur:', event); - // Pas besoin de reconnecter ici, onclose sera appelé juste après - }; - - ws.onclose = (event) => { - isConnecting = false; - stopHeartbeat(); - console.warn(`[WS] ⚠️ Déconnecté (Code: ${event.code}). Reconnexion dans ${reconnectInterval / 1000}s...`); - - // Reconnexion exponentielle (1s, 1.5s, 2.25s...) - setTimeout(() => { - connect(); - reconnectInterval = Math.min(reconnectInterval * 1.5, MAX_RECONNECT_INTERVAL); - }, reconnectInterval); - }; -} - -function startHeartbeat() { - stopHeartbeat(); - // Envoie un ping toutes les 30 secondes pour éviter que le serveur ou le navigateur ne coupe la connexion - pingIntervalId = setInterval(() => { - if (ws && ws.readyState === WebSocket.OPEN) { - // Adapter selon ce que ton serveur attend comme Ping, ou envoyer un message vide - // ws.send(JSON.stringify({ flag: 'Ping', content: '' })); - } - }, APP_CONFIG.TIMEOUTS.WS_HEARTBEAT); -} - -function stopHeartbeat() { - if (pingIntervalId) clearInterval(pingIntervalId); -} - -export function sendMessage(flag: AnkFlag, message: string): void { - if (ws && ws.readyState === WebSocket.OPEN) { - const networkMessage = { - flag: flag, - content: message, - }; - ws.send(JSON.stringify(networkMessage)); - } else { - console.warn(`[WS] Pas connecté. Message '${flag}' mis en file d'attente.`); - const networkMessage = { - flag: flag, - content: message, - }; - messageQueue.push(JSON.stringify(networkMessage)); - - // Si on n'est pas déjà en train de se connecter, on force une tentative - if (!isConnecting) connect(); - } -} - -export function getUrl(): string { - return urlReference; -} - -export function close(): void { - if (ws) { - ws.onclose = null; // On évite la reconnexion auto si fermeture volontaire - stopHeartbeat(); - ws.close(); - } -} diff --git a/src/workers/core.worker.ts b/src/workers/core.worker.ts new file mode 100644 index 0000000..c2d29b2 --- /dev/null +++ b/src/workers/core.worker.ts @@ -0,0 +1,859 @@ +import * as Comlink from 'comlink'; +import { + ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, + OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff +} from '../../pkg/sdk_client'; +import Database from '../services/database.service'; +import { storeData, retrieveData } from '../services/storage.service'; +import { BackUp } from '../types/index'; +import { APP_CONFIG } from '../config/constants'; +import { splitPrivateData } from '../utils/service.utils'; + +// Services internes au worker +import { SdkService } from '../services/core/sdk.service'; +import { WalletService } from '../services/domain/wallet.service'; +import { ProcessService } from '../services/domain/process.service'; +import { CryptoService } from '../services/domain/crypto.service'; + +export class CoreBackend { + // Services + private sdkService: SdkService; + private walletService!: WalletService; + private processService!: ProcessService; + private cryptoService: CryptoService; + private db!: Database; + + // État (State) + private processId: string | null = null; + private stateId: string | null = null; + private membersList: Record = {}; + private notifications: any[] | null = null; + private currentBlockHeight: number = -1; + private pendingKeyRequests: Map void> = new Map(); + + // Flags publics (State) + public device1: boolean = false; + public device2Ready: boolean = false; + + private isInitialized = false; + + // Callbacks vers le Main Thread + private notifier: ((event: string, data?: any) => void) | null = null; + private networkSender: ((flag: string, content: string) => void) | null = null; + private relayUpdater: ((url: string, sp: string) => void) | null = null; + private relayGetter: (() => Promise) | null = null; + + constructor() { + this.sdkService = new SdkService(); + this.cryptoService = new CryptoService(this.sdkService); + // Initialisation temporaire + this.walletService = new WalletService(this.sdkService, null as any); + this.processService = new ProcessService(this.sdkService, null as any); + } + + public async init(): Promise { + if (this.isInitialized) return; + + console.log('[CoreWorker] ⚙️ Initialisation du Backend...'); + await this.sdkService.init(); // Charge le WASM + this.db = await Database.getInstance(); // Lance le Database Worker + + this.walletService = new WalletService(this.sdkService, this.db); + this.processService = new ProcessService(this.sdkService, this.db); + + this.notifications = this.getNotifications(); + this.isInitialized = true; + console.log('[CoreWorker] ✅ Backend prêt.'); + } + + // --- CONFIGURATION DES CALLBACKS --- + public setCallbacks( + notifier: (event: string, data?: any) => void, + networkSender: (flag: string, content: string) => void, + relayUpdater: (url: string, sp: string) => void, + relayGetter: () => Promise + ) { + this.notifier = notifier; + this.networkSender = networkSender; + this.relayUpdater = relayUpdater; + this.relayGetter = relayGetter; + } + + // ========================================== + // GETTERS & SETTERS (STATE) + // ========================================== + public setProcessId(id: string | null) { this.processId = id; } + public setStateId(id: string | null) { this.stateId = id; } + public getProcessId() { return this.processId; } + public getStateId() { return this.stateId; } + + public getDevice1() { return this.device1; } // Ajouté + public getDevice2Ready() { return this.device2Ready; } // Ajouté + + public resetState() { + this.device1 = false; + this.device2Ready = false; + } + + // ========================================== + // WALLET PROXY + // ========================================== + public isPaired() { return this.walletService.isPaired(); } + public getAmount() { return this.walletService.getAmount(); } + public getDeviceAddress() { return this.walletService.getDeviceAddress(); } + public dumpDeviceFromMemory() { return this.walletService.dumpDeviceFromMemory(); } + public dumpNeuteredDevice() { return this.walletService.dumpNeuteredDevice(); } + public getPairingProcessId() { return this.walletService.getPairingProcessId(); } + public async getDeviceFromDatabase() { return this.walletService.getDeviceFromDatabase(); } + public restoreDevice(d: Device) { this.walletService.restoreDevice(d); } + public pairDevice(pid: string, list: string[]) { this.walletService.pairDevice(pid, list); } + public async unpairDevice() { await this.walletService.unpairDevice(); } + public async saveDeviceInDatabase(d: Device) { await this.walletService.saveDeviceInDatabase(d); } + public async createNewDevice() { + return this.walletService.createNewDevice(this.currentBlockHeight > 0 ? this.currentBlockHeight : 0); + } + public async dumpWallet() { return this.walletService.dumpWallet(); } + public async getMemberFromDevice() { return this.walletService.getMemberFromDevice(); } + + // ========================================== + // PROCESS PROXY + // ========================================== + public async getProcess(id: string) { return this.processService.getProcess(id); } + public async getProcesses() { return this.processService.getProcesses(); } + public async restoreProcessesFromDB() { await this.processService.getProcesses(); } + public getLastCommitedState(p: Process) { return this.processService.getLastCommitedState(p); } + public getUncommitedStates(p: Process) { return this.processService.getUncommitedStates(p); } + public getStateFromId(p: Process, id: string) { return this.processService.getStateFromId(p, id); } + public getRoles(p: Process) { return this.processService.getRoles(p); } + public getLastCommitedStateIndex(p: Process) { return this.processService.getLastCommitedStateIndex(p); } + public async batchSaveProcessesToDb(p: Record) { return this.processService.batchSaveProcesses(p); } + + // ========================================== + // CRYPTO HELPERS + // ========================================== + public decodeValue(val: number[]) { return this.sdkService.decodeValue(val); } + public hexToBlob(hex: string) { return this.cryptoService.hexToBlob(hex); } + public hexToUInt8Array(hex: string) { return this.cryptoService.hexToUInt8Array(hex); } + public async blobToHex(blob: Blob) { return this.cryptoService.blobToHex(blob); } + public getHashForFile(c: string, l: string, f: any) { return this.cryptoService.getHashForFile(c, l, f); } + public getMerkleProofForFile(s: ProcessState, a: string) { return this.cryptoService.getMerkleProofForFile(s, a); } + public validateMerkleProof(p: MerkleProofResult, h: string) { return this.cryptoService.validateMerkleProof(p, h); } + private splitData(obj: Record) { return this.cryptoService.splitData(obj); } + + // ========================================== + // MEMBERS + // ========================================== + public getAllMembers() { return this.membersList; } + public getAllMembersSorted() { + return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))); + } + public async ensureMembersAvailable(): Promise { + if (Object.keys(this.membersList).length > 0) return; + console.warn('[CoreWorker] Tentative de récupération des membres...'); + } + public getAddressesForMemberId(memberId: string): string[] | null { + if (!this.membersList[memberId]) return null; + return this.membersList[memberId].sp_addresses; + } + public compareMembers(memberA: string[], memberB: string[]): boolean { + if (!memberA || !memberB) return false; + if (memberA.length !== memberB.length) return false; + return memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item)); + } + + // ========================================== + // UTILITAIRES DIVERS + // ========================================== + public createFaucetMessage() { + return this.sdkService.getClient().create_faucet_msg(); + } + + public isChildRole(parent: any, child: any): boolean { + try { + this.sdkService.getClient().is_child_role(JSON.stringify(parent), JSON.stringify(child)); + return true; + } catch (e) { + console.error(e); + return false; + } + } + + // ========================================== + // LOGIQUE HANDSHAKE + // ========================================== + public async handleHandshakeMsg(url: string, parsedMsg: any) { + try { + const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); + + if (handshakeMsg.sp_address && this.relayUpdater) { + await this.relayUpdater(url, handshakeMsg.sp_address); + } + this.currentBlockHeight = handshakeMsg.chain_tip; + + if (!this.isPaired()) { + console.log(`[CoreWorker] ⏳ Non pairé. Attente appairage...`); + } + + this.updateDeviceBlockHeight(); + + if (handshakeMsg.peers_list) { + this.membersList = { ...this.membersList, ...handshakeMsg.peers_list as Record }; + } + + if (handshakeMsg.processes_list) { + await this.syncProcessesFromHandshake(handshakeMsg.processes_list); + } + } catch (e) { + console.error("Handshake Error", e); + } + } + + public async updateDeviceBlockHeight() { + if (this.currentBlockHeight <= 0) return; + const device = await this.walletService.getDeviceFromDatabase(); + if (!device) return; + + if (device.sp_wallet.birthday === 0) { + device.sp_wallet.birthday = this.currentBlockHeight; + device.sp_wallet.last_scan = this.currentBlockHeight; + await this.walletService.saveDeviceInDatabase(device); + this.walletService.restoreDevice(device); + } else if (device.sp_wallet.last_scan < this.currentBlockHeight) { + console.log(`[CoreWorker] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`); + try { + await this.sdkService.getClient().scan_blocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT); + const updatedDevice = this.walletService.dumpDeviceFromMemory(); + await this.walletService.saveDeviceInDatabase(updatedDevice); + } catch (e) { + console.error('Scan error', e); + } + } + } + + private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap) { + if (!newProcesses || Object.keys(newProcesses).length === 0) return; + console.log(`[CoreWorker] Synchro ${Object.keys(newProcesses).length} processus...`); + + const toSave: Record = {}; + const currentProcesses = await this.getProcesses(); + + if (Object.keys(currentProcesses).length === 0) { + await this.processService.batchSaveProcesses(newProcesses); + } else { + for (const [processId, process] of Object.entries(newProcesses)) { + const existing = currentProcesses[processId]; + if (existing) { + let newStates: string[] = []; + let newRoles: Record[] = []; + + for (const state of process.states) { + if (!state || !state.state_id) continue; + + if (state.state_id === APP_CONFIG.EMPTY_32_BYTES) { + const existingTip = existing.states[existing.states.length - 1].commited_in; + if (existingTip !== state.commited_in) { + existing.states.pop(); + existing.states.push(state); + toSave[processId] = existing; + } + } else if (!this.processService.getStateFromId(existing, state.state_id)) { + const existingLast = existing.states.pop(); + if (existingLast) { + existing.states.push(state); + existing.states.push(existingLast); + toSave[processId] = existing; + if (this.rolesContainsUs(state.roles)) { + newStates.push(state.state_id); + newRoles.push(state.roles); + } + } + } else { + const existingState = this.processService.getStateFromId(existing, state.state_id); + if (existingState && (!existingState.keys || Object.keys(existingState.keys).length === 0)) { + if (this.rolesContainsUs(state.roles)) { + newStates.push(state.state_id); + newRoles.push(state.roles); + } + } + } + } + + if (newStates.length > 0) { + await this.ensureConnections(existing); + await this.requestDataFromPeers(processId, newStates, newRoles); + } + } else { + toSave[processId] = process; + } + } + if (Object.keys(toSave).length > 0) { + await this.processService.batchSaveProcesses(toSave); + } + } + if (this.notifier) this.notifier('processes-updated'); + } + + // ========================================== + // LOGIQUE MÉTIER + // ========================================== + public async getMyProcesses(): Promise { + try { + const pid = this.getPairingProcessId(); + return await this.processService.getMyProcesses(pid); + } catch (e) { + return null; + } + } + + public async ensureConnections(process: Process, stateId: string | null = null): Promise { + console.info(`[CoreWorker] 🔄 Check connexions (StateID: ${stateId || 'default'})`); + if (!process) return; + + let state: ProcessState | null = null; + if (stateId) state = this.processService.getStateFromId(process, stateId); + if (!state && process.states.length >= 2) state = process.states[process.states.length - 2]; + if (!state) return; + + await this.ensureMembersAvailable(); + const members = new Set(); + + if (state.roles) { + for (const role of Object.values(state.roles)) { + for (const memberId of role.members) { + const addrs = this.getAddressesForMemberId(memberId); + if (addrs) members.add({ sp_addresses: addrs }); + } + } + } + + if (members.size === 0) { + let publicData: Record | null = null; + for (let i = process.states.length - 1; i >= 0; i--) { + const s = process.states[i]; + if (s.public_data && s.public_data['pairedAddresses']) { + publicData = s.public_data; + break; + } + } + if (publicData && publicData['pairedAddresses']) { + const decoded = this.decodeValue(publicData['pairedAddresses']); + if (decoded) members.add({ sp_addresses: decoded }); + } + } + + if (members.size === 0) return; + + const unconnected = new Set(); + const myAddress = this.getDeviceAddress(); + for (const member of Array.from(members)) { + if (!member.sp_addresses) continue; + for (const address of member.sp_addresses) { + if (address === myAddress) continue; + if ((await this.getSecretForAddress(address)) === null) unconnected.add(address); + } + } + + if (unconnected.size > 0) { + console.log(`[CoreWorker] 📡 ${unconnected.size} non connectés. Connexion...`); + await this.connectAddresses(Array.from(unconnected)); + } + } + + public async connectAddresses(addresses: string[]): Promise { + if (addresses.length === 0) return null; + const feeRate = APP_CONFIG.FEE_RATE; + try { + return this.sdkService.getClient().create_transaction(addresses, feeRate); + } catch (error: any) { + if (String(error).includes('Insufficient funds')) { + await this.getTokensFromFaucet(); + return this.sdkService.getClient().create_transaction(addresses, feeRate); + } else { + throw error; + } + } + } + + private async getTokensFromFaucet(): Promise { + console.log('[CoreWorker] 🚰 Demande Faucet...'); + const availableAmt = this.getAmount(); + const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10); + if (availableAmt < target) { + const msg = this.sdkService.getClient().create_faucet_msg(); + if (this.networkSender) this.networkSender('Faucet', msg); + + let attempts = 3; + while (attempts > 0) { + if (this.getAmount() >= target) return; + attempts--; + await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY)); + } + throw new Error('Montant insuffisant après faucet'); + } + } + + public async createPairingProcess(userName: string, pairWith: string[]): Promise { + if (this.isPaired()) throw new Error('Déjà appairé'); + const myAddress = this.getDeviceAddress(); + pairWith.push(myAddress); + const privateData = { description: 'pairing', counter: 0 }; + const publicData = { memberPublicName: userName, pairedAddresses: pairWith }; + const validation_fields = [...Object.keys(privateData), ...Object.keys(publicData), 'roles']; + const roles = { + pairing: { + members: [], + validation_rules: [{ quorum: 1.0, fields: validation_fields, min_sig_member: 1.0 }], + storages: [APP_CONFIG.URLS.STORAGE], + }, + }; + return this.createProcess(privateData, publicData, roles); + } + + public async createProcess(privateData: any, publicData: any, roles: any, feeRate = APP_CONFIG.FEE_RATE): Promise { + // Appel au main thread pour avoir l'adresse du relais + const relay = this.relayGetter ? await this.relayGetter() : ''; + if (!relay) throw new Error("Aucun relais disponible"); + + const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); + const members = this.membersList; + try { + return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members); + } catch (e: any) { + if (String(e).includes('Insufficient funds')) { + await this.getTokensFromFaucet(); + return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members); + } + throw e; + } + } + + private async attemptCreateProcess(priv: any, roles: any, pub: any, relay: string, fee: number, members: any): Promise { + const res = this.sdkService.getClient().create_new_process(priv, roles, pub, relay, fee, members); + if (res.updated_process) { + await this.ensureConnections(res.updated_process.current_process); + } + return res; + } + + public async updateProcess(processId: string, newData: any, privateFields: string[], roles: any): Promise { + const process = await this.processService.getProcess(processId); + if (!process) throw new Error('Process not found'); + + let lastState = this.processService.getLastCommitedState(process); + let currentProcess = process; + + if (!lastState) { + const first = process.states[0]; + if (this.rolesContainsUs(first.roles)) { + const appRes = await this.approveChange(processId, first.state_id); + await this.handleApiReturn(appRes); + const prdRes = await this.createPrdUpdate(processId, first.state_id); + await this.handleApiReturn(prdRes); + } else if (first.validation_tokens.length > 0) { + const res = await this.createPrdUpdate(processId, first.state_id); + await this.handleApiReturn(res); + } + const updated = await this.processService.getProcess(processId); + if (updated) currentProcess = updated; + lastState = this.processService.getLastCommitedState(currentProcess); + if (!lastState) throw new Error('Still no commited state'); + } + + const lastStateIndex = this.getLastCommitedStateIndex(currentProcess); + if (lastStateIndex === null) throw new Error('Index commited introuvable'); + + const privateData: any = {}; + const publicData: any = {}; + + 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; + } + let isPrivate = false; + for (let i = lastStateIndex; i >= 0; i--) { + if (currentProcess.states[i].pcd_commitment[field]) { + privateData[field] = newData[field]; + isPrivate = true; + break; + } + } + if (!isPrivate) publicData[field] = newData[field]; + } + + const finalRoles = roles || this.processService.getRoles(currentProcess); + const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); + + const res = this.sdkService.getClient().update_process(currentProcess, encodedPrivateData, finalRoles, encodedPublicData, this.membersList); + if (res.updated_process) await this.ensureConnections(res.updated_process.current_process); + return res; + } + + private async prepareProcessData(priv: any, pub: any) { + const p1 = this.splitData(priv); + const p2 = this.splitData(pub); + return { + encodedPrivateData: { ...this.sdkService.getClient().encode_json(p1.jsonCompatibleData), ...this.sdkService.getClient().encode_binary(p1.binaryData) }, + encodedPublicData: { ...this.sdkService.getClient().encode_json(p2.jsonCompatibleData), ...this.sdkService.getClient().encode_binary(p2.binaryData) }, + }; + } + + // ========================================== + // API METHODS (Actions) + // ========================================== + public async createPrdUpdate(pid: string, sid: string) { + const p = await this.getProcess(pid); + await this.ensureConnections(p!); + return this.sdkService.getClient().create_update_message(p, sid, this.membersList); + } + public async createPrdResponse(pid: string, sid: string) { + const p = await this.getProcess(pid); + return this.sdkService.getClient().create_response_prd(p, sid, this.membersList); + } + public async approveChange(pid: string, sid: string) { + const p = await this.getProcess(pid); + const res = this.sdkService.getClient().validate_state(p, sid, this.membersList); + if (res.updated_process) await this.ensureConnections(res.updated_process.current_process); + return res; + } + public async rejectChange(pid: string, sid: string) { + const p = await this.getProcess(pid); + return this.sdkService.getClient().refuse_state(p, sid); + } + public async requestDataFromPeers(pid: string, sids: string[], roles: any) { + const res = this.sdkService.getClient().request_data(pid, sids, roles, this.membersList); + await this.handleApiReturn(res); + } + + public async resetDevice() { + this.sdkService.getClient().reset_device(); + await this.db.clearMultipleStores(['wallet', 'shared_secrets', 'unconfirmed_secrets', 'processes', 'diffs']); + } + + public async handleApiReturn(res: ApiReturn) { + if (!res || Object.keys(res).length === 0) return; + try { + const txData = (res.partial_tx ? await this.handlePartialTx(res.partial_tx) : null) || res.new_tx_to_send; + if (txData && txData.transaction.length != 0) { + if (this.networkSender) this.networkSender('NewTx', JSON.stringify(txData)); + await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY)); + } + if (res.secrets) await this.handleSecrets(res.secrets); + if (res.updated_process) await this.handleUpdatedProcess(res.updated_process); + if (res.push_to_storage) await this.handlePushToStorage(res.push_to_storage); + + if (res.commit_to_send && this.networkSender) this.networkSender('Commit', JSON.stringify(res.commit_to_send)); + if (res.ciphers_to_send && this.networkSender) for (const c of res.ciphers_to_send) this.networkSender('Cipher', c); + } catch (e) { + console.error('ApiReturn Error:', e); + } + } + + private async handlePartialTx(partialTx: any): Promise { + try { + return this.sdkService.getClient().sign_transaction(partialTx).new_tx_to_send; + } catch (e) { + return null; + } + } + + private async handleSecrets(secrets: any) { + const { unconfirmed_secrets, shared_secrets } = secrets; + const unconfirmedList = unconfirmed_secrets && unconfirmed_secrets.length > 0 ? unconfirmed_secrets : []; + const sharedList = shared_secrets && Object.keys(shared_secrets).length > 0 + ? Object.entries(shared_secrets).map(([key, value]) => ({ key, value })) + : []; + + if (unconfirmedList.length > 0 || sharedList.length > 0) { + await this.db.saveSecretsBatch(unconfirmedList, sharedList); + } + } + + private async handleUpdatedProcess(updated: any) { + const pid = updated.process_id; + if (updated.encrypted_data) { + for (const [h, c] of Object.entries(updated.encrypted_data as Record)) await this.saveBlobToDb(h, this.hexToBlob(c)); + } + await this.processService.saveProcessToDb(pid, updated.current_process); + if (updated.diffs) await this.saveDiffsToDb(updated.diffs); + + this._resolvePendingKeyRequests(pid, updated.current_process); + // Notification UI + if (this.notifier) this.notifier('processes-updated'); + } + + public async saveDiffsToDb(diffs: UserDiff[]) { + await this.db.saveDiffs(diffs); + } + + private _resolvePendingKeyRequests(processId: string, process: Process) { + if (this.pendingKeyRequests.size === 0) return; + for (const state of process.states) { + if (!state.keys) continue; + for (const [attr, key] of Object.entries(state.keys)) { + const rid = `${processId}_${state.state_id}_${attr}`; + if (this.pendingKeyRequests.has(rid)) { + this.pendingKeyRequests.get(rid)?.(key as string); + this.pendingKeyRequests.delete(rid); + } + } + } + } + + private async handlePushToStorage(hashes: string[]) { + for (const hash of hashes) { + try { + const blob = await this.getBlobFromDb(hash); + const diff = await this.getDiffByValue(hash); + if (blob && diff) await this.saveDataToStorage(diff.storages, hash, blob, null); + } catch (e) { + console.error('Push error', e); + } + } + } + + public async handleCommitError(response: string) { + const content = JSON.parse(response); + const errorMsg = content.error['GenericError']; + if (!['State is identical to the previous state', 'Not enough valid proofs'].includes(errorMsg)) { + // Retry via network callback + if (this.networkSender) { + setTimeout(() => this.networkSender!('Commit', JSON.stringify(content)), APP_CONFIG.TIMEOUTS.RETRY_DELAY); + } + } + } + + public rolesContainsUs(roles: any) { + return this.processService.rolesContainsMember(roles, this.getPairingProcessId()); + } + + public async getSecretForAddress(address: string): Promise { + return await this.db.getSharedSecret(address); + } + + public async getAllDiffs(): Promise> { + return await this.db.getAllDiffs(); + } + + public async getDiffByValue(value: string): Promise { + return await this.db.getDiff(value); + } + + public async getAllSecrets(): Promise { + return await this.db.getAllSecrets(); + } + + // ========================================== + // STORAGE & DB + // ========================================== + public async saveBlobToDb(h: string, d: Blob) { + await this.db.saveBlob(h, d); + } + public async getBlobFromDb(h: string) { + return await this.db.getBlob(h); + } + public async fetchValueFromStorage(h: string) { + return retrieveData([APP_CONFIG.URLS.STORAGE], h); + } + public async saveDataToStorage(s: string[], h: string, d: Blob, ttl: number | null) { + return storeData(s, h, d, ttl); + } + + // ========================================== + // HELPERS + // ========================================== + public getProcessName(p: Process) { + const pub = this.getPublicData(p); + if (pub && pub['processName']) return this.decodeValue(pub['processName']); + return null; + } + public getPublicData(p: Process) { + const last = this.getLastCommitedState(p); + return last ? last.public_data : p.states[0]?.public_data || null; + } + public updateMemberPublicName(pid: string, name: string) { + return this.updateProcess(pid, { memberPublicName: name }, [], null); + } + + // ========================================== + // UI HELPERS + // ========================================== + public getNotifications() { + return this.notifications; + } + public setNotifications(n: any[]) { + this.notifications = n; + } + + // ========================================== + // PARSING & RESEAU ENTRANT + // ========================================== + async parseCipher(msg: string) { + try { + const res = this.sdkService.getClient().parse_cipher(msg, this.membersList, await this.getProcesses()); + await this.handleApiReturn(res); + } catch (e) { + console.error('Cipher Error', e); + } + } + + async parseNewTx(msg: string) { + const parsed = JSON.parse(msg); + if (parsed.error) return; + + const prevouts = this.sdkService.getClient().get_prevouts(parsed.transaction); + for (const p of Object.values(await this.getProcesses())) { + const tip = p.states[p.states.length - 1].commited_in; + if (prevouts.includes(tip)) { + const newTip = this.sdkService.getClient().get_txid(parsed.transaction); + const newStateId = this.sdkService.getClient().get_opreturn(parsed.transaction); + this.sdkService.getClient().process_commit_new_state(p, newStateId, newTip); + break; + } + } + + try { + const res = this.sdkService.getClient().parse_new_tx(msg, 0, this.membersList); + if (res && (res.partial_tx || res.new_tx_to_send || res.secrets || res.updated_process)) { + await this.handleApiReturn(res); + const d = this.dumpDeviceFromMemory(); + const old = await this.getDeviceFromDatabase(); + if (old && old.pairing_process_commitment) d.pairing_process_commitment = old.pairing_process_commitment; + await this.saveDeviceInDatabase(d); + } + } catch (e) { } + } + + // ========================================== + // BACKUP & RESTORE + // ========================================== + public async importJSON(backup: BackUp) { + await this.resetDevice(); + await this.walletService.saveDeviceInDatabase(backup.device); + this.walletService.restoreDevice(backup.device); + await this.processService.batchSaveProcesses(backup.processes); + await this.restoreSecretsFromBackUp(backup.secrets); + } + public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { + const sharedList = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value })); + await this.db.saveSecretsBatch(secretsStore.unconfirmed_secrets, sharedList); + await this.restoreSecretsFromDB(); + } + public async restoreSecretsFromDB() { + const secretsStore = await this.db.getAllSecrets(); + this.sdkService.getClient().set_shared_secrets(JSON.stringify(secretsStore)); + console.log("[CoreWorker] 🔐 Secrets restaurés depuis la DB"); + } + public async createBackUp() { + const device = await this.walletService.getDeviceFromDatabase(); + if (!device) return null; + return { device, processes: await this.processService.getProcesses(), secrets: await this.getAllSecrets() }; + } + + // ========================================== + // DECRYPT ATTRIBUTE + // ========================================== + public async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { + console.groupCollapsed(`[CoreWorker] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`); + + try { + let hash: string | null | undefined = state.pcd_commitment[attribute]; + let key: string | null | undefined = state.keys[attribute]; + const pairingProcessId = this.getPairingProcessId(); + + if (!hash) { + console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); + return null; + } + + if (!key) { + if (!this._checkAccess(state, attribute, pairingProcessId)) { + console.log(`⛔ Accès non autorisé. Abandon.`); + return null; + } + const result = await this._fetchMissingKey(processId, state, attribute); + hash = result.hash; + key = result.key; + } + + if (hash && key) { + const blob = await this.getBlobFromDb(hash); + if (!blob) { + console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`); + return null; + } + + try { + const buf = await blob.arrayBuffer(); + const cipher = new Uint8Array(buf); + const keyUIntArray = this.hexToUInt8Array(key); + + const clear = this.sdkService.getClient().decrypt_data(keyUIntArray, cipher); + if (!clear) throw new Error('decrypt_data returned null'); + + const decoded = this.sdkService.getClient().decode_value(clear); + console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); + return decoded; + } catch (e) { + console.error(`💥 Échec du déchiffrement: ${e}`); + return null; + } + } + return null; + } catch (error) { + console.error(`💥 Erreur:`, error); + return null; + } finally { + console.groupEnd(); + } + } + + private _checkAccess(state: ProcessState, attribute: string, pairingProcessId: string): boolean { + const roles = state.roles; + return Object.values(roles).some((role) => { + const isMember = role.members.includes(pairingProcessId); + if (!isMember) return false; + return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute)); + }); + } + + private async _fetchMissingKey(processId: string, state: ProcessState, attribute: string): Promise<{ hash: string | null; key: string | null }> { + try { + const process = await this.getProcess(processId); + if (!process) return { hash: null, key: null }; + + await this.ensureConnections(process); + await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); + + const requestId = `${processId}_${state.state_id}_${attribute}`; + const keyRequestPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.pendingKeyRequests.delete(requestId); + reject(new Error(`Timeout waiting for key: ${attribute}`)); + }, APP_CONFIG.TIMEOUTS.KEY_REQUEST); + + this.pendingKeyRequests.set(requestId, (key: string) => { + clearTimeout(timeout); + resolve(key); + }); + }); + + const receivedKey = await keyRequestPromise; + const updatedProcess = await this.getProcess(processId); + if (!updatedProcess) return { hash: null, key: null }; + + const updatedState = this.getStateFromId(updatedProcess, state.state_id); + const updatedHash = updatedState ? updatedState.pcd_commitment[attribute] : state.pcd_commitment[attribute]; + + return { hash: updatedHash, key: receivedKey }; + } catch (e) { + return { hash: null, key: null }; + } + } +} + +Comlink.expose(new CoreBackend()); \ No newline at end of file diff --git a/src/workers/network.worker.ts b/src/workers/network.worker.ts new file mode 100644 index 0000000..d576930 --- /dev/null +++ b/src/workers/network.worker.ts @@ -0,0 +1,150 @@ +import * as Comlink from 'comlink'; +import { APP_CONFIG } from '../config/constants'; + +// On redéfinit le type localement pour éviter d'importer tout le SDK WASM ici +type AnkFlag = 'Handshake' | 'NewTx' | 'Cipher' | 'Commit' | 'Faucet' | 'Ping'; + +export class NetworkBackend { + private sockets: Map = new Map(); + private relayAddresses: Map = new Map(); // wsUrl -> spAddress + private messageQueue: string[] = []; + + // Callback pour notifier le Main Thread + private msgCallback: ((flag: string, content: string, url: string) => void) | null = null; + private statusCallback: ((url: string, status: 'OPEN' | 'CLOSED', spAddress?: string) => void) | null = null; + + // Timers pour la gestion des reconnexions + private reconnectTimers: Map = new Map(); + private heartbeatInterval: any = null; + + constructor() { + this.startHeartbeat(); + } + + public setCallbacks( + msgCb: (flag: string, content: string, url: string) => void, + statusCb: (url: string, status: 'OPEN' | 'CLOSED', spAddress?: string) => void + ) { + this.msgCallback = msgCb; + this.statusCallback = statusCb; + } + + public async connect(url: string) { + if (this.sockets.has(url) && this.sockets.get(url)?.readyState === WebSocket.OPEN) return; + + console.log(`[NetworkWorker] 🔌 Connexion à ${url}...`); + const ws = new WebSocket(url); + + ws.onopen = () => { + console.log(`[NetworkWorker] ✅ Connecté à ${url}`); + this.sockets.set(url, ws); + + // Reset timer reconnexion si existant + if (this.reconnectTimers.has(url)) { + clearTimeout(this.reconnectTimers.get(url)); + this.reconnectTimers.delete(url); + } + + // Vider la file d'attente (si message en attente pour ce socket ou broadcast) + this.flushQueue(); + + if (this.statusCallback) this.statusCallback(url, 'OPEN'); + }; + + ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + // Si c'est un Handshake, on met à jour la map locale + if (msg.flag === 'Handshake' && msg.content) { + const handshake = JSON.parse(msg.content); + if (handshake.sp_address) { + this.relayAddresses.set(url, handshake.sp_address); + if (this.statusCallback) this.statusCallback(url, 'OPEN', handshake.sp_address); + } + } + + // On remonte TOUT au Main Thread (qui passera au Core) + if (this.msgCallback) this.msgCallback(msg.flag, msg.content, url); + } catch (e) { + console.error('[NetworkWorker] Erreur parsing message:', e); + } + }; + + ws.onerror = (e) => { + // console.error(`[NetworkWorker] Erreur sur ${url}`, e); + }; + + ws.onclose = () => { + console.warn(`[NetworkWorker] ❌ Déconnecté de ${url}.`); + this.sockets.delete(url); + this.relayAddresses.set(url, ''); // Reset spAddress + + if (this.statusCallback) this.statusCallback(url, 'CLOSED'); + this.scheduleReconnect(url); + }; + } + + public sendMessage(flag: AnkFlag, content: string) { + const msgStr = JSON.stringify({ flag, content }); + + // Stratégie simple : On envoie à TOUS les relais connectés (Broadcast) + // Ou on pourrait cibler un relais spécifique si besoin. + let sent = false; + for (const [url, ws] of this.sockets) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(msgStr); + sent = true; + } + } + + if (!sent) { + // console.warn(`[NetworkWorker] Pas de connexion. Message ${flag} mis en file.`); + this.messageQueue.push(msgStr); + } + } + + public getAvailableRelay(): string | null { + // Retourne l'adresse SP d'un relais connecté + for (const sp of this.relayAddresses.values()) { + if (sp && sp !== '') return sp; + } + return null; + } + + public getAllRelays() { + return Object.fromEntries(this.relayAddresses); + } + + // --- INTERNES --- + + private flushQueue() { + while (this.messageQueue.length > 0) { + const msg = this.messageQueue.shift(); + if (!msg) break; + for (const ws of this.sockets.values()) { + if (ws.readyState === WebSocket.OPEN) ws.send(msg); + } + } + } + + private scheduleReconnect(url: string) { + if (this.reconnectTimers.has(url)) return; + + console.log(`[NetworkWorker] ⏳ Reconnexion à ${url} dans 3s...`); + const timer = setTimeout(() => { + this.reconnectTimers.delete(url); + this.connect(url); + }, 3000); // Délai fixe ou APP_CONFIG.TIMEOUTS.RETRY_DELAY + + this.reconnectTimers.set(url, timer); + } + + private startHeartbeat() { + this.heartbeatInterval = setInterval(() => { + // Envoi d'un ping léger ou gestion du keep-alive + // this.sendMessage('Ping', ''); + }, 30000); + } +} + +Comlink.expose(new NetworkBackend()); \ No newline at end of file